Compare commits

...

53 Commits

Author SHA1 Message Date
ece060d03d feat: fix XML bug in GenerateCasToken() 2023-09-15 18:57:59 +08:00
1276da4daa Check old password for normal user in SetPassword() 2023-09-15 10:21:02 +08:00
616629ef99 Refactor CheckLoginPermission() code 2023-09-15 02:47:53 +08:00
b633ecdcf2 Fix bug that cannot access application's public certificate for non "admin" owner 2023-09-15 00:56:40 +08:00
a12ba7fb85 feat: allow CORS for UserInfo API in OIDC (#2313) 2023-09-13 18:11:13 +08:00
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
bb04b10e8b Fix JSON issue in GenerateCasToken() 2023-09-13 16:45:11 +08:00
ea1414dfd0 Fix typo 2023-09-13 00:19:32 +08:00
32a8a028d5 Set TOTP issuer to Casdoor 2023-09-12 23:56:39 +08:00
0fe34c2f53 Fix the issue that database syncer can't work with null-fields on source table 2023-09-12 16:06:44 +08:00
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
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
e5e264628e Remove "RUN mkdir tempFiles" 2023-09-09 20:24:18 +08:00
8d4127f744 feat: improve dashboard UI for mobile devices (#2320) 2023-09-09 16:17:24 +08:00
1305899060 Fix "app" user API denied issue 2023-09-09 15:44:36 +08:00
411a85c7ab Remove useless GetMaxLenStr() 2023-09-09 15:40:35 +08:00
f39358e122 Improve SMS Test's initial value 2023-09-09 02:38:15 +08:00
a84752bbb5 Update go-sms-sender to v0.14.0 2023-09-09 02:15:38 +08:00
e9d8ab8cdb fix: hide tour component for mobile (#2317) 2023-09-08 22:53:46 +08:00
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
c62588f9bc Add EmailVerified to UserInfo 2023-09-08 18:27:14 +08:00
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
7318ee6e3a Improve LocalFileSystemProvider's error handling 2023-09-07 10:49:39 +08:00
3459ef1479 Improve termsOfUse UI and error handling 2023-09-07 10:33:20 +08:00
ca6b27f922 feat: fix notification provider frontend bug and twitter error (#2310) 2023-09-06 23:41:34 +08:00
e528e8883b Add "localhost" to IsRedirectUriValid() 2023-09-06 21:14:58 +08:00
b7cd604e56 Mask user in GenerateCasToken() 2023-09-06 18:36:55 +08:00
3c2fd574a6 Refactor GenerateCasToken() 2023-09-06 18:35:13 +08:00
a9de7d3aef Add groups to permission 2023-09-06 00:10:33 +08:00
9820801634 Make Product's Providers longer (255) 2023-09-05 20:24:24 +08:00
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
bc8e9cfd64 feat: storage provider's domain initial value bug (#2303) 2023-09-05 14:53:32 +08:00
c1eae9fcd8 Fix TotpMfa's Verify() 2023-09-04 19:21:26 +08:00
6dae6e4954 docs: fix all dead links (#2297)
https://github.com/Selflocking/linkchecker/actions/runs/6058177987
2023-09-03 21:19:23 +08:00
559a91e8ee feat: fix bug that failed to set password after changing username (#2296)
* fix: failed to set password after changing username

When we add a new member to an organization using Casdoor, Casdoor will automatically generate a member with a random username, such as "user_qvducc". When we change the username, for example, to "yunshu", an issue arises where we are unable to successfully edit the password. This is because Casdoor searches for a user based on `owner/username`, and before any changes are saved, the username in the database remains "user_qvducc". However, the frontend uses `orgName/yunshu` instead of `orgName/user_qvducc` to send the request to change the password. As a result, the backend cannot find the user and the password change fails.

* Update user.go

---------

Co-authored-by: hsluoyz <hsluoyz@qq.com>
2023-09-03 00:04:48 +08:00
b0aaf09ef1 Add 7 new i18n languages 2023-09-02 18:49:43 +08:00
7e2f67c49a Fix i18n error 2023-09-02 18:33:19 +08:00
e584a6a111 Support using "?allowEmpty=1" to bypass empty displayName check in update-user API 2023-09-02 11:59:07 +08:00
6700d2e244 fix: show error when frontend HTML entry does not exist (#2289)
* fix: add response when web file not found

The error flow is as follows:

Assuming my directory structure is as follows:

```tree
├── GitHub
│   ├── casdoor  # code repository
              ├── casdoor # compiled binary file
```

Execute the program in the `GitHub` directory:

```bash
./casdoor/casdoor
```

The working directory at this time is `GitHub`.

According to the code:

```go
func StaticFilter(ctx *context.Context) {
	urlPath := ctx.Request.URL.Path

   /// omitted

	path := "web/build"
	if urlPath == "/" {
		path += "/index.html"
	} else {
		path += urlPath
	}

	if !util.FileExist(path) {
		path = "web/build/index.html"
	}
	if !util.FileExist(path) {
		return
	}

    /// omitted
}
```

If the user accesses `/`, according to this code, the returned value is actually `web/build/index.html`. But the current directory is GitHub, and there is no `web/build/index.html` file. According to the following code, it will directly return:

```go
	if !util.FileExist(path) {
		return
	}
```

Then in `main.go`:

```go
	beego.InsertFilter("*", beego.BeforeRouter, routers.StaticFilter)
	beego.InsertFilter("*", beego.BeforeRouter, routers.AutoSigninFilter)
	beego.InsertFilter("*", beego.BeforeRouter, routers.CorsFilter)
	beego.InsertFilter("*", beego.BeforeRouter, routers.ApiFilter)
	beego.InsertFilter("*", beego.BeforeRouter, routers.PrometheusFilter)
	beego.InsertFilter("*", beego.BeforeRouter, routers.RecordMessage)
```

The introduction of `beego.InsertFilter` is as follows:

```
func InsertFilter(pattern string, pos int, filter FilterFunc, params ...bool) *App

InsertFilter adds a FilterFunc with pattern condition and action constant. The pos means action constant including beego.BeforeStatic, beego.BeforeRouter, beego.BeforeExec, beego.AfterExec and beego.FinishRouter. The bool params is for setting the returnOnOutput value (false allows multiple filters to execute)
```

When the `params` parameter is `false`, it runs multiple filters. The default is `true`.

So normally, if

```go
beego.InsertFilter("*", beego.BeforeRouter, routers.StaticFilter)
```

response something, the following filters will not be executed. But because the file does not exist, the function directly returns, causing the subsequent filters to continue executing. When it reaches

```go
beego.InsertFilter("*", beego.BeforeRouter, routers.ApiFilter)
```

it will start to check permissions:

```
subOwner = anonymous, subName = anonymous, method = GET, urlPath = /login, obj.Owner = , obj.Name = , result = deny
```

Then it will report this error:

```json
{
    "status": "error",
    "msg": "Unauthorized operation",
    "data": null,
    "data2": null
}
```

The solution should be:

```go
func StaticFilter(ctx *context.Context) {
	urlPath := ctx.Request.URL.Path

   /// omitted

	path := "web/build"
	if urlPath == "/" {
		path += "/index.html"
	} else {
		path += urlPath
	}

	if !util.FileExist(path) {
		// todo: response error: page not found
		return
	}

    /// omitted
}
```

* Update static_filter.go

---------

Co-authored-by: hsluoyz <hsluoyz@qq.com>
2023-09-02 00:06:04 +08:00
0c5c308071 fix: sendCasAuthenticationResponseErr when pgtUrlObj if not valid url (#2287)
* fix: sendCasAuthenticationResponseErr when pgtUrlObj if not valid url

check pgtUrlObj.Scheme first will cause panic if url.Parse returns error.

* Update cas.go

---------

Co-authored-by: hsluoyz <hsluoyz@qq.com>
2023-09-01 22:26:57 +08:00
0b859197da Fix CAS "/proxyValidate" API 2023-09-01 21:47:26 +08:00
3078409343 Add CertPublicKey to Application 2023-09-01 21:16:51 +08:00
bbf2db2e00 feat: support to use a different db schema for pg (#2281) 2023-09-01 18:02:13 +08:00
0c7b911ce7 Fix enforcer edit page logic 2023-09-01 01:30:50 +08:00
2cc55715ac Add app.conf existence check 2023-09-01 01:25:45 +08:00
c829bf1769 Fix DummyPaymentProvider's return URL 2023-09-01 01:25:15 +08:00
ec956c12ca Fix Email duplicated issue in update-user 2023-08-31 23:44:40 +08:00
d3d4646c56 feat: fix can not create db when using pg with a dbname in DSN (#2280)
* fix: can not create db when using pg with a dbname in DSN

* Update ormer.go

---------

Co-authored-by: hsluoyz <hsluoyz@qq.com>
2023-08-31 18:05:38 +08:00
669ac7c618 Don't encrypt user pass when user.PasswordType is non-empty when adding users 2023-08-31 17:49:36 +08:00
6715efd781 Fix enforcer edit page 2023-08-31 17:32:36 +08:00
953be4a7b6 feat: support subscription periods (yearly/monthly) (#2265)
* feat: support year/month subscription

* feat: add GetPrice() for plan

* feat: add GetDuration

* feat: gofumpt

* feat: add subscription mode for pricing

* feat: restrict auto create product operation

* fix: format code

* feat: add period for plan,remove period from pricing

* feat: format code

* feat: remove space

* feat: remove period in signup page
2023-08-30 17:13:45 +08:00
943cc43427 Fix payment list and product edit actions 2023-08-28 21:01:23 +08:00
1e5ce7a045 Fix crash in syncUsersNoError() 2023-08-28 01:51:06 +08:00
120 changed files with 9568 additions and 865 deletions

View File

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

View File

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

View File

@ -62,13 +62,13 @@ func (c *ApiController) GetApplications() {
} }
paginator := pagination.SetPaginator(c.Ctx, limit, count) paginator := pagination.SetPaginator(c.Ctx, limit, count)
app, err := object.GetPaginationApplications(owner, paginator.Offset(), limit, field, value, sortField, sortOrder) application, err := object.GetPaginationApplications(owner, paginator.Offset(), limit, field, value, sortField, sortOrder)
if err != nil { if err != nil {
c.ResponseError(err.Error()) c.ResponseError(err.Error())
return return
} }
applications := object.GetMaskedApplications(app, userId) applications := object.GetMaskedApplications(application, userId)
c.ResponseOk(applications, paginator.Nums()) c.ResponseOk(applications, paginator.Nums())
} }
} }
@ -84,13 +84,33 @@ func (c *ApiController) GetApplication() {
userId := c.GetSessionUsername() userId := c.GetSessionUsername()
id := c.Input().Get("id") id := c.Input().Get("id")
app, err := object.GetApplication(id) application, err := object.GetApplication(id)
if err != nil { if err != nil {
c.ResponseError(err.Error()) c.ResponseError(err.Error())
return return
} }
c.ResponseOk(object.GetMaskedApplication(app, userId)) 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
}
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))
} }
// GetUserApplication // GetUserApplication
@ -164,13 +184,13 @@ func (c *ApiController) GetOrganizationApplications() {
} }
paginator := pagination.SetPaginator(c.Ctx, limit, count) paginator := pagination.SetPaginator(c.Ctx, limit, count)
app, err := object.GetPaginationOrganizationApplications(owner, organization, paginator.Offset(), limit, field, value, sortField, sortOrder) application, err := object.GetPaginationOrganizationApplications(owner, organization, paginator.Offset(), limit, field, value, sortField, sortOrder)
if err != nil { if err != nil {
c.ResponseError(err.Error()) c.ResponseError(err.Error())
return return
} }
applications := object.GetMaskedApplications(app, userId) applications := object.GetMaskedApplications(application, userId)
c.ResponseOk(applications, paginator.Nums()) c.ResponseOk(applications, paginator.Nums())
} }
} }

View File

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

View File

@ -35,6 +35,11 @@ const (
UnauthorizedService string = "UNAUTHORIZED_SERVICE" UnauthorizedService string = "UNAUTHORIZED_SERVICE"
) )
func queryUnescape(service string) string {
s, _ := url.QueryUnescape(service)
return s
}
func (c *RootController) CasValidate() { func (c *RootController) CasValidate() {
ticket := c.Input().Get("ticket") ticket := c.Input().Get("ticket")
service := c.Input().Get("service") service := c.Input().Get("service")
@ -60,24 +65,25 @@ func (c *RootController) CasServiceValidate() {
if !strings.HasPrefix(ticket, "ST") { if !strings.HasPrefix(ticket, "ST") {
c.sendCasAuthenticationResponseErr(InvalidTicket, fmt.Sprintf("Ticket %s not recognized", ticket), format) c.sendCasAuthenticationResponseErr(InvalidTicket, fmt.Sprintf("Ticket %s not recognized", ticket), format)
} }
c.CasP3ServiceAndProxyValidate() c.CasP3ProxyValidate()
} }
func (c *RootController) CasProxyValidate() { func (c *RootController) CasProxyValidate() {
// https://apereo.github.io/cas/6.6.x/protocol/CAS-Protocol-Specification.html#26-proxyvalidate-cas-20
// "/proxyValidate" should accept both service tickets and proxy tickets.
c.CasP3ProxyValidate()
}
func (c *RootController) CasP3ServiceValidate() {
ticket := c.Input().Get("ticket") ticket := c.Input().Get("ticket")
format := c.Input().Get("format") format := c.Input().Get("format")
if !strings.HasPrefix(ticket, "PT") { if !strings.HasPrefix(ticket, "ST") {
c.sendCasAuthenticationResponseErr(InvalidTicket, fmt.Sprintf("Ticket %s not recognized", ticket), format) c.sendCasAuthenticationResponseErr(InvalidTicket, fmt.Sprintf("Ticket %s not recognized", ticket), format)
} }
c.CasP3ServiceAndProxyValidate() c.CasP3ProxyValidate()
} }
func queryUnescape(service string) string { func (c *RootController) CasP3ProxyValidate() {
s, _ := url.QueryUnescape(service)
return s
}
func (c *RootController) CasP3ServiceAndProxyValidate() {
ticket := c.Input().Get("ticket") ticket := c.Input().Get("ticket")
format := c.Input().Get("format") format := c.Input().Get("format")
service := c.Input().Get("service") service := c.Input().Get("service")
@ -115,15 +121,17 @@ func (c *RootController) CasP3ServiceAndProxyValidate() {
pgtiou := serviceResponse.Success.ProxyGrantingTicket pgtiou := serviceResponse.Success.ProxyGrantingTicket
// todo: check whether it is https // todo: check whether it is https
pgtUrlObj, err := url.Parse(pgtUrl) pgtUrlObj, err := url.Parse(pgtUrl)
if err != nil {
c.sendCasAuthenticationResponseErr(InvalidProxyCallback, err.Error(), format)
return
}
if pgtUrlObj.Scheme != "https" { if pgtUrlObj.Scheme != "https" {
c.sendCasAuthenticationResponseErr(InvalidProxyCallback, "callback is not https", format) c.sendCasAuthenticationResponseErr(InvalidProxyCallback, "callback is not https", format)
return return
} }
// make a request to pgturl passing pgt and pgtiou // make a request to pgturl passing pgt and pgtiou
if err != nil {
c.sendCasAuthenticationResponseErr(InternalError, err.Error(), format)
return
}
param := pgtUrlObj.Query() param := pgtUrlObj.Query()
param.Add("pgtId", pgt) param.Add("pgtId", pgt)
param.Add("pgtIou", pgtiou) param.Add("pgtIou", pgtiou)
@ -263,7 +271,6 @@ func (c *RootController) sendCasAuthenticationResponseErr(code, msg, format stri
Message: msg, Message: msg,
}, },
} }
if format == "json" { if format == "json" {
c.Data["json"] = serviceResponse c.Data["json"] = serviceResponse
c.ServeJSON() c.ServeJSON()

View File

@ -83,7 +83,7 @@ func (c *ApiController) GetEnforcer() {
return return
} }
if loadModelCfg == "true" { if loadModelCfg == "true" && enforcer.Model != "" {
err := enforcer.LoadModelCfg() err := enforcer.LoadModelCfg()
if err != nil { if err != nil {
return return

View File

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

View File

@ -114,7 +114,7 @@ func (c *ApiController) GetPlan() {
// @router /update-plan [post] // @router /update-plan [post]
func (c *ApiController) UpdatePlan() { func (c *ApiController) UpdatePlan() {
id := c.Input().Get("id") id := c.Input().Get("id")
owner := util.GetOwnerFromId(id)
var plan object.Plan var plan object.Plan
err := json.Unmarshal(c.Ctx.Input.RequestBody, &plan) err := json.Unmarshal(c.Ctx.Input.RequestBody, &plan)
if err != nil { if err != nil {
@ -122,17 +122,19 @@ func (c *ApiController) UpdatePlan() {
return return
} }
if plan.Product != "" { if plan.Product != "" {
planId := util.GetId(plan.Owner, plan.Product) productId := util.GetId(owner, plan.Product)
product, err := object.GetProduct(planId) product, err := object.GetProduct(productId)
if err != nil { if err != nil {
c.ResponseError(err.Error()) c.ResponseError(err.Error())
return return
} }
object.UpdateProductForPlan(&plan, product) if product != nil {
_, err = object.UpdateProduct(planId, product) object.UpdateProductForPlan(&plan, product)
if err != nil { _, err = object.UpdateProduct(productId, product)
c.ResponseError(err.Error()) if err != nil {
return c.ResponseError(err.Error())
return
}
} }
} }
c.Data["json"] = wrapActionResponse(object.UpdatePlan(id, &plan)) c.Data["json"] = wrapActionResponse(object.UpdatePlan(id, &plan))

View File

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

View File

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

View File

@ -258,6 +258,13 @@ func (c *ApiController) UpdateUser() {
return return
} }
if c.Input().Get("allowEmpty") == "" {
if user.DisplayName == "" {
c.ResponseError(c.T("user:Display name cannot be empty"))
return
}
}
if msg := object.CheckUpdateUser(oldUser, &user, c.GetAcceptLanguage()); msg != "" { if msg := object.CheckUpdateUser(oldUser, &user, c.GetAcceptLanguage()); msg != "" {
c.ResponseError(msg) c.ResponseError(msg)
return return
@ -441,12 +448,25 @@ func (c *ApiController) SetPassword() {
} }
targetUser, err := object.GetUser(userId) targetUser, err := object.GetUser(userId)
if targetUser == nil {
c.ResponseError(fmt.Sprintf(c.T("general:The user: %s doesn't exist"), userId))
return
}
if err != nil { if err != nil {
c.ResponseError(err.Error()) c.ResponseError(err.Error())
return return
} }
if oldPassword != "" { isAdmin := c.IsAdmin()
if isAdmin {
if oldPassword != "" {
msg := object.CheckPassword(targetUser, oldPassword, c.GetAcceptLanguage())
if msg != "" {
c.ResponseError(msg)
return
}
}
} else {
msg := object.CheckPassword(targetUser, oldPassword, c.GetAcceptLanguage()) msg := object.CheckPassword(targetUser, oldPassword, c.GetAcceptLanguage())
if msg != "" { if msg != "" {
c.ResponseError(msg) c.ResponseError(msg)

227
email/azure_acs.go Normal file
View File

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

27
email/provider.go Normal file
View File

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

49
email/smtp.go Normal file
View File

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

19
go.mod
View File

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

392
go.sum

File diff suppressed because it is too large Load Diff

View File

@ -38,7 +38,13 @@ func TestGenerateI18nFrontend(t *testing.T) {
applyToOtherLanguage("frontend", "tr", data) applyToOtherLanguage("frontend", "tr", data)
applyToOtherLanguage("frontend", "ar", data) applyToOtherLanguage("frontend", "ar", data)
applyToOtherLanguage("frontend", "he", data) applyToOtherLanguage("frontend", "he", data)
applyToOtherLanguage("frontend", "nl", data)
applyToOtherLanguage("frontend", "pl", data)
applyToOtherLanguage("frontend", "fi", data) applyToOtherLanguage("frontend", "fi", data)
applyToOtherLanguage("frontend", "sv", data)
applyToOtherLanguage("frontend", "uk", data)
applyToOtherLanguage("frontend", "kk", data)
applyToOtherLanguage("frontend", "fa", data)
} }
func TestGenerateI18nBackend(t *testing.T) { func TestGenerateI18nBackend(t *testing.T) {
@ -60,5 +66,11 @@ func TestGenerateI18nBackend(t *testing.T) {
applyToOtherLanguage("backend", "tr", data) applyToOtherLanguage("backend", "tr", data)
applyToOtherLanguage("backend", "ar", data) applyToOtherLanguage("backend", "ar", data)
applyToOtherLanguage("backend", "he", data) applyToOtherLanguage("backend", "he", data)
applyToOtherLanguage("backend", "nl", data)
applyToOtherLanguage("backend", "pl", data)
applyToOtherLanguage("backend", "fi", data) applyToOtherLanguage("backend", "fi", data)
applyToOtherLanguage("backend", "sv", data)
applyToOtherLanguage("backend", "uk", data)
applyToOtherLanguage("backend", "kk", data)
applyToOtherLanguage("backend", "fa", data)
} }

142
i18n/locales/fa/data.json Normal file
View File

@ -0,0 +1,142 @@
{
"account": {
"Failed to add user": "Failed to add user",
"Get init score failed, error: %w": "Get init score failed, error: %w",
"Please sign out first": "Please sign out first",
"The application does not allow to sign up new account": "The application does not allow to sign up new account"
},
"auth": {
"Challenge method should be S256": "Challenge method should be S256",
"Failed to create user, user information is invalid: %s": "Failed to create user, user information is invalid: %s",
"Failed to login in: %s": "Failed to login in: %s",
"Invalid token": "Invalid token",
"State expected: %s, but got: %s": "State expected: %s, but got: %s",
"The account for provider: %s and username: %s (%s) does not exist and is not allowed to sign up as new account via %%s, please use another way to sign up": "The account for provider: %s and username: %s (%s) does not exist and is not allowed to sign up as new account via %%s, please use another way to sign up",
"The account for provider: %s and username: %s (%s) does not exist and is not allowed to sign up as new account, please contact your IT support": "The account for provider: %s and username: %s (%s) does not exist and is not allowed to sign up as new account, please contact your IT support",
"The account for provider: %s and username: %s (%s) is already linked to another account: %s (%s)": "The account for provider: %s and username: %s (%s) is already linked to another account: %s (%s)",
"The application: %s does not exist": "The application: %s does not exist",
"The login method: login with password is not enabled for the application": "The login method: login with password is not enabled for the application",
"The provider: %s is not enabled for the application": "The provider: %s is not enabled for the application",
"Unauthorized operation": "Unauthorized operation",
"Unknown authentication type (not password or provider), form = %s": "Unknown authentication type (not password or provider), form = %s",
"User's tag: %s is not listed in the application's tags": "User's tag: %s is not listed in the application's tags"
},
"cas": {
"Service %s and %s do not match": "Service %s and %s do not match"
},
"check": {
"Affiliation cannot be blank": "Affiliation cannot be blank",
"DisplayName cannot be blank": "DisplayName cannot be blank",
"DisplayName is not valid real name": "DisplayName is not valid real name",
"Email already exists": "Email already exists",
"Email cannot be empty": "Email cannot be empty",
"Email is invalid": "Email is invalid",
"Empty username.": "Empty username.",
"FirstName cannot be blank": "FirstName cannot be blank",
"LDAP user name or password incorrect": "LDAP user name or password incorrect",
"LastName cannot be blank": "LastName cannot be blank",
"Multiple accounts with same uid, please check your ldap server": "Multiple accounts with same uid, please check your ldap server",
"Organization does not exist": "Organization does not exist",
"Password must have at least 6 characters": "Password must have at least 6 characters",
"Phone already exists": "Phone already exists",
"Phone cannot be empty": "Phone cannot be empty",
"Phone number is invalid": "Phone number is invalid",
"Session outdated, please login again": "Session outdated, please login again",
"The user is forbidden to sign in, please contact the administrator": "The user is forbidden to sign in, please contact the administrator",
"The user: %s doesn't exist in LDAP server": "The user: %s doesn't exist in LDAP server",
"The username may only contain alphanumeric characters, underlines or hyphens, cannot have consecutive hyphens or underlines, and cannot begin or end with a hyphen or underline.": "The username may only contain alphanumeric characters, underlines or hyphens, cannot have consecutive hyphens or underlines, and cannot begin or end with a hyphen or underline.",
"Username already exists": "Username already exists",
"Username cannot be an email address": "Username cannot be an email address",
"Username cannot contain white spaces": "Username cannot contain white spaces",
"Username cannot start with a digit": "Username cannot start with a digit",
"Username is too long (maximum is 39 characters).": "Username is too long (maximum is 39 characters).",
"Username must have at least 2 characters": "Username must have at least 2 characters",
"You have entered the wrong password or code too many times, please wait for %d minutes and try again": "You have entered the wrong password or code too many times, please wait for %d minutes and try again",
"Your region is not allow to signup by phone": "Your region is not allow to signup by phone",
"password or code is incorrect": "password or code is incorrect",
"password or code is incorrect, you have %d remaining chances": "password or code is incorrect, you have %d remaining chances",
"unsupported password type: %s": "unsupported password type: %s"
},
"general": {
"Missing parameter": "Missing parameter",
"Please login first": "Please login first",
"The user: %s doesn't exist": "The user: %s doesn't exist",
"don't support captchaProvider: ": "don't support captchaProvider: ",
"this operation is not allowed in demo mode": "this operation is not allowed in demo mode"
},
"ldap": {
"Ldap server exist": "Ldap server exist"
},
"link": {
"Please link first": "Please link first",
"This application has no providers": "This application has no providers",
"This application has no providers of type": "This application has no providers of type",
"This provider can't be unlinked": "This provider can't be unlinked",
"You are not the global admin, you can't unlink other users": "You are not the global admin, you can't unlink other users",
"You can't unlink yourself, you are not a member of any application": "You can't unlink yourself, you are not a member of any application"
},
"organization": {
"Only admin can modify the %s.": "Only admin can modify the %s.",
"The %s is immutable.": "The %s is immutable.",
"Unknown modify rule %s.": "Unknown modify rule %s."
},
"provider": {
"Invalid application id": "Invalid application id",
"the provider: %s does not exist": "the provider: %s does not exist"
},
"resource": {
"User is nil for tag: avatar": "User is nil for tag: avatar",
"Username or fullFilePath is empty: username = %s, fullFilePath = %s": "Username or fullFilePath is empty: username = %s, fullFilePath = %s"
},
"saml": {
"Application %s not found": "Application %s not found"
},
"saml_sp": {
"provider %s's category is not SAML": "provider %s's category is not SAML"
},
"service": {
"Empty parameters for emailForm: %v": "Empty parameters for emailForm: %v",
"Invalid Email receivers: %s": "Invalid Email receivers: %s",
"Invalid phone receivers: %s": "Invalid phone receivers: %s"
},
"storage": {
"The objectKey: %s is not allowed": "The objectKey: %s is not allowed",
"The provider type: %s is not supported": "The provider type: %s is not supported"
},
"token": {
"Empty clientId or clientSecret": "Empty clientId or clientSecret",
"Grant_type: %s is not supported in this application": "Grant_type: %s is not supported in this application",
"Invalid application or wrong clientSecret": "Invalid application or wrong clientSecret",
"Invalid client_id": "Invalid client_id",
"Redirect URI: %s doesn't exist in the allowed Redirect URI list": "Redirect URI: %s doesn't exist in the allowed Redirect URI list",
"Token not found, invalid accessToken": "Token not found, invalid accessToken"
},
"user": {
"Display name cannot be empty": "Display name cannot be empty",
"New password cannot contain blank space.": "New password cannot contain blank space."
},
"user_upload": {
"Failed to import users": "Failed to import users"
},
"util": {
"No application is found for userId: %s": "No application is found for userId: %s",
"No provider for category: %s is found for application: %s": "No provider for category: %s is found for application: %s",
"The provider: %s is not found": "The provider: %s is not found"
},
"verification": {
"Code has not been sent yet!": "Code has not been sent yet!",
"Invalid captcha provider.": "Invalid captcha provider.",
"Phone number is invalid in your region %s": "Phone number is invalid in your region %s",
"Turing test failed.": "Turing test failed.",
"Unable to get the email modify rule.": "Unable to get the email modify rule.",
"Unable to get the phone modify rule.": "Unable to get the phone modify rule.",
"Unknown type": "Unknown type",
"Wrong verification code!": "Wrong verification code!",
"You should verify your code in %d min!": "You should verify your code in %d min!",
"the user does not exist, please sign up first": "the user does not exist, please sign up first"
},
"webauthn": {
"Found no credentials for this user": "Found no credentials for this user",
"Please call WebAuthnSigninBegin first": "Please call WebAuthnSigninBegin first"
}
}

142
i18n/locales/kk/data.json Normal file
View File

@ -0,0 +1,142 @@
{
"account": {
"Failed to add user": "Failed to add user",
"Get init score failed, error: %w": "Get init score failed, error: %w",
"Please sign out first": "Please sign out first",
"The application does not allow to sign up new account": "The application does not allow to sign up new account"
},
"auth": {
"Challenge method should be S256": "Challenge method should be S256",
"Failed to create user, user information is invalid: %s": "Failed to create user, user information is invalid: %s",
"Failed to login in: %s": "Failed to login in: %s",
"Invalid token": "Invalid token",
"State expected: %s, but got: %s": "State expected: %s, but got: %s",
"The account for provider: %s and username: %s (%s) does not exist and is not allowed to sign up as new account via %%s, please use another way to sign up": "The account for provider: %s and username: %s (%s) does not exist and is not allowed to sign up as new account via %%s, please use another way to sign up",
"The account for provider: %s and username: %s (%s) does not exist and is not allowed to sign up as new account, please contact your IT support": "The account for provider: %s and username: %s (%s) does not exist and is not allowed to sign up as new account, please contact your IT support",
"The account for provider: %s and username: %s (%s) is already linked to another account: %s (%s)": "The account for provider: %s and username: %s (%s) is already linked to another account: %s (%s)",
"The application: %s does not exist": "The application: %s does not exist",
"The login method: login with password is not enabled for the application": "The login method: login with password is not enabled for the application",
"The provider: %s is not enabled for the application": "The provider: %s is not enabled for the application",
"Unauthorized operation": "Unauthorized operation",
"Unknown authentication type (not password or provider), form = %s": "Unknown authentication type (not password or provider), form = %s",
"User's tag: %s is not listed in the application's tags": "User's tag: %s is not listed in the application's tags"
},
"cas": {
"Service %s and %s do not match": "Service %s and %s do not match"
},
"check": {
"Affiliation cannot be blank": "Affiliation cannot be blank",
"DisplayName cannot be blank": "DisplayName cannot be blank",
"DisplayName is not valid real name": "DisplayName is not valid real name",
"Email already exists": "Email already exists",
"Email cannot be empty": "Email cannot be empty",
"Email is invalid": "Email is invalid",
"Empty username.": "Empty username.",
"FirstName cannot be blank": "FirstName cannot be blank",
"LDAP user name or password incorrect": "LDAP user name or password incorrect",
"LastName cannot be blank": "LastName cannot be blank",
"Multiple accounts with same uid, please check your ldap server": "Multiple accounts with same uid, please check your ldap server",
"Organization does not exist": "Organization does not exist",
"Password must have at least 6 characters": "Password must have at least 6 characters",
"Phone already exists": "Phone already exists",
"Phone cannot be empty": "Phone cannot be empty",
"Phone number is invalid": "Phone number is invalid",
"Session outdated, please login again": "Session outdated, please login again",
"The user is forbidden to sign in, please contact the administrator": "The user is forbidden to sign in, please contact the administrator",
"The user: %s doesn't exist in LDAP server": "The user: %s doesn't exist in LDAP server",
"The username may only contain alphanumeric characters, underlines or hyphens, cannot have consecutive hyphens or underlines, and cannot begin or end with a hyphen or underline.": "The username may only contain alphanumeric characters, underlines or hyphens, cannot have consecutive hyphens or underlines, and cannot begin or end with a hyphen or underline.",
"Username already exists": "Username already exists",
"Username cannot be an email address": "Username cannot be an email address",
"Username cannot contain white spaces": "Username cannot contain white spaces",
"Username cannot start with a digit": "Username cannot start with a digit",
"Username is too long (maximum is 39 characters).": "Username is too long (maximum is 39 characters).",
"Username must have at least 2 characters": "Username must have at least 2 characters",
"You have entered the wrong password or code too many times, please wait for %d minutes and try again": "You have entered the wrong password or code too many times, please wait for %d minutes and try again",
"Your region is not allow to signup by phone": "Your region is not allow to signup by phone",
"password or code is incorrect": "password or code is incorrect",
"password or code is incorrect, you have %d remaining chances": "password or code is incorrect, you have %d remaining chances",
"unsupported password type: %s": "unsupported password type: %s"
},
"general": {
"Missing parameter": "Missing parameter",
"Please login first": "Please login first",
"The user: %s doesn't exist": "The user: %s doesn't exist",
"don't support captchaProvider: ": "don't support captchaProvider: ",
"this operation is not allowed in demo mode": "this operation is not allowed in demo mode"
},
"ldap": {
"Ldap server exist": "Ldap server exist"
},
"link": {
"Please link first": "Please link first",
"This application has no providers": "This application has no providers",
"This application has no providers of type": "This application has no providers of type",
"This provider can't be unlinked": "This provider can't be unlinked",
"You are not the global admin, you can't unlink other users": "You are not the global admin, you can't unlink other users",
"You can't unlink yourself, you are not a member of any application": "You can't unlink yourself, you are not a member of any application"
},
"organization": {
"Only admin can modify the %s.": "Only admin can modify the %s.",
"The %s is immutable.": "The %s is immutable.",
"Unknown modify rule %s.": "Unknown modify rule %s."
},
"provider": {
"Invalid application id": "Invalid application id",
"the provider: %s does not exist": "the provider: %s does not exist"
},
"resource": {
"User is nil for tag: avatar": "User is nil for tag: avatar",
"Username or fullFilePath is empty: username = %s, fullFilePath = %s": "Username or fullFilePath is empty: username = %s, fullFilePath = %s"
},
"saml": {
"Application %s not found": "Application %s not found"
},
"saml_sp": {
"provider %s's category is not SAML": "provider %s's category is not SAML"
},
"service": {
"Empty parameters for emailForm: %v": "Empty parameters for emailForm: %v",
"Invalid Email receivers: %s": "Invalid Email receivers: %s",
"Invalid phone receivers: %s": "Invalid phone receivers: %s"
},
"storage": {
"The objectKey: %s is not allowed": "The objectKey: %s is not allowed",
"The provider type: %s is not supported": "The provider type: %s is not supported"
},
"token": {
"Empty clientId or clientSecret": "Empty clientId or clientSecret",
"Grant_type: %s is not supported in this application": "Grant_type: %s is not supported in this application",
"Invalid application or wrong clientSecret": "Invalid application or wrong clientSecret",
"Invalid client_id": "Invalid client_id",
"Redirect URI: %s doesn't exist in the allowed Redirect URI list": "Redirect URI: %s doesn't exist in the allowed Redirect URI list",
"Token not found, invalid accessToken": "Token not found, invalid accessToken"
},
"user": {
"Display name cannot be empty": "Display name cannot be empty",
"New password cannot contain blank space.": "New password cannot contain blank space."
},
"user_upload": {
"Failed to import users": "Failed to import users"
},
"util": {
"No application is found for userId: %s": "No application is found for userId: %s",
"No provider for category: %s is found for application: %s": "No provider for category: %s is found for application: %s",
"The provider: %s is not found": "The provider: %s is not found"
},
"verification": {
"Code has not been sent yet!": "Code has not been sent yet!",
"Invalid captcha provider.": "Invalid captcha provider.",
"Phone number is invalid in your region %s": "Phone number is invalid in your region %s",
"Turing test failed.": "Turing test failed.",
"Unable to get the email modify rule.": "Unable to get the email modify rule.",
"Unable to get the phone modify rule.": "Unable to get the phone modify rule.",
"Unknown type": "Unknown type",
"Wrong verification code!": "Wrong verification code!",
"You should verify your code in %d min!": "You should verify your code in %d min!",
"the user does not exist, please sign up first": "the user does not exist, please sign up first"
},
"webauthn": {
"Found no credentials for this user": "Found no credentials for this user",
"Please call WebAuthnSigninBegin first": "Please call WebAuthnSigninBegin first"
}
}

142
i18n/locales/nl/data.json Normal file
View File

@ -0,0 +1,142 @@
{
"account": {
"Failed to add user": "Failed to add user",
"Get init score failed, error: %w": "Get init score failed, error: %w",
"Please sign out first": "Please sign out first",
"The application does not allow to sign up new account": "The application does not allow to sign up new account"
},
"auth": {
"Challenge method should be S256": "Challenge method should be S256",
"Failed to create user, user information is invalid: %s": "Failed to create user, user information is invalid: %s",
"Failed to login in: %s": "Failed to login in: %s",
"Invalid token": "Invalid token",
"State expected: %s, but got: %s": "State expected: %s, but got: %s",
"The account for provider: %s and username: %s (%s) does not exist and is not allowed to sign up as new account via %%s, please use another way to sign up": "The account for provider: %s and username: %s (%s) does not exist and is not allowed to sign up as new account via %%s, please use another way to sign up",
"The account for provider: %s and username: %s (%s) does not exist and is not allowed to sign up as new account, please contact your IT support": "The account for provider: %s and username: %s (%s) does not exist and is not allowed to sign up as new account, please contact your IT support",
"The account for provider: %s and username: %s (%s) is already linked to another account: %s (%s)": "The account for provider: %s and username: %s (%s) is already linked to another account: %s (%s)",
"The application: %s does not exist": "The application: %s does not exist",
"The login method: login with password is not enabled for the application": "The login method: login with password is not enabled for the application",
"The provider: %s is not enabled for the application": "The provider: %s is not enabled for the application",
"Unauthorized operation": "Unauthorized operation",
"Unknown authentication type (not password or provider), form = %s": "Unknown authentication type (not password or provider), form = %s",
"User's tag: %s is not listed in the application's tags": "User's tag: %s is not listed in the application's tags"
},
"cas": {
"Service %s and %s do not match": "Service %s and %s do not match"
},
"check": {
"Affiliation cannot be blank": "Affiliation cannot be blank",
"DisplayName cannot be blank": "DisplayName cannot be blank",
"DisplayName is not valid real name": "DisplayName is not valid real name",
"Email already exists": "Email already exists",
"Email cannot be empty": "Email cannot be empty",
"Email is invalid": "Email is invalid",
"Empty username.": "Empty username.",
"FirstName cannot be blank": "FirstName cannot be blank",
"LDAP user name or password incorrect": "LDAP user name or password incorrect",
"LastName cannot be blank": "LastName cannot be blank",
"Multiple accounts with same uid, please check your ldap server": "Multiple accounts with same uid, please check your ldap server",
"Organization does not exist": "Organization does not exist",
"Password must have at least 6 characters": "Password must have at least 6 characters",
"Phone already exists": "Phone already exists",
"Phone cannot be empty": "Phone cannot be empty",
"Phone number is invalid": "Phone number is invalid",
"Session outdated, please login again": "Session outdated, please login again",
"The user is forbidden to sign in, please contact the administrator": "The user is forbidden to sign in, please contact the administrator",
"The user: %s doesn't exist in LDAP server": "The user: %s doesn't exist in LDAP server",
"The username may only contain alphanumeric characters, underlines or hyphens, cannot have consecutive hyphens or underlines, and cannot begin or end with a hyphen or underline.": "The username may only contain alphanumeric characters, underlines or hyphens, cannot have consecutive hyphens or underlines, and cannot begin or end with a hyphen or underline.",
"Username already exists": "Username already exists",
"Username cannot be an email address": "Username cannot be an email address",
"Username cannot contain white spaces": "Username cannot contain white spaces",
"Username cannot start with a digit": "Username cannot start with a digit",
"Username is too long (maximum is 39 characters).": "Username is too long (maximum is 39 characters).",
"Username must have at least 2 characters": "Username must have at least 2 characters",
"You have entered the wrong password or code too many times, please wait for %d minutes and try again": "You have entered the wrong password or code too many times, please wait for %d minutes and try again",
"Your region is not allow to signup by phone": "Your region is not allow to signup by phone",
"password or code is incorrect": "password or code is incorrect",
"password or code is incorrect, you have %d remaining chances": "password or code is incorrect, you have %d remaining chances",
"unsupported password type: %s": "unsupported password type: %s"
},
"general": {
"Missing parameter": "Missing parameter",
"Please login first": "Please login first",
"The user: %s doesn't exist": "The user: %s doesn't exist",
"don't support captchaProvider: ": "don't support captchaProvider: ",
"this operation is not allowed in demo mode": "this operation is not allowed in demo mode"
},
"ldap": {
"Ldap server exist": "Ldap server exist"
},
"link": {
"Please link first": "Please link first",
"This application has no providers": "This application has no providers",
"This application has no providers of type": "This application has no providers of type",
"This provider can't be unlinked": "This provider can't be unlinked",
"You are not the global admin, you can't unlink other users": "You are not the global admin, you can't unlink other users",
"You can't unlink yourself, you are not a member of any application": "You can't unlink yourself, you are not a member of any application"
},
"organization": {
"Only admin can modify the %s.": "Only admin can modify the %s.",
"The %s is immutable.": "The %s is immutable.",
"Unknown modify rule %s.": "Unknown modify rule %s."
},
"provider": {
"Invalid application id": "Invalid application id",
"the provider: %s does not exist": "the provider: %s does not exist"
},
"resource": {
"User is nil for tag: avatar": "User is nil for tag: avatar",
"Username or fullFilePath is empty: username = %s, fullFilePath = %s": "Username or fullFilePath is empty: username = %s, fullFilePath = %s"
},
"saml": {
"Application %s not found": "Application %s not found"
},
"saml_sp": {
"provider %s's category is not SAML": "provider %s's category is not SAML"
},
"service": {
"Empty parameters for emailForm: %v": "Empty parameters for emailForm: %v",
"Invalid Email receivers: %s": "Invalid Email receivers: %s",
"Invalid phone receivers: %s": "Invalid phone receivers: %s"
},
"storage": {
"The objectKey: %s is not allowed": "The objectKey: %s is not allowed",
"The provider type: %s is not supported": "The provider type: %s is not supported"
},
"token": {
"Empty clientId or clientSecret": "Empty clientId or clientSecret",
"Grant_type: %s is not supported in this application": "Grant_type: %s is not supported in this application",
"Invalid application or wrong clientSecret": "Invalid application or wrong clientSecret",
"Invalid client_id": "Invalid client_id",
"Redirect URI: %s doesn't exist in the allowed Redirect URI list": "Redirect URI: %s doesn't exist in the allowed Redirect URI list",
"Token not found, invalid accessToken": "Token not found, invalid accessToken"
},
"user": {
"Display name cannot be empty": "Display name cannot be empty",
"New password cannot contain blank space.": "New password cannot contain blank space."
},
"user_upload": {
"Failed to import users": "Failed to import users"
},
"util": {
"No application is found for userId: %s": "No application is found for userId: %s",
"No provider for category: %s is found for application: %s": "No provider for category: %s is found for application: %s",
"The provider: %s is not found": "The provider: %s is not found"
},
"verification": {
"Code has not been sent yet!": "Code has not been sent yet!",
"Invalid captcha provider.": "Invalid captcha provider.",
"Phone number is invalid in your region %s": "Phone number is invalid in your region %s",
"Turing test failed.": "Turing test failed.",
"Unable to get the email modify rule.": "Unable to get the email modify rule.",
"Unable to get the phone modify rule.": "Unable to get the phone modify rule.",
"Unknown type": "Unknown type",
"Wrong verification code!": "Wrong verification code!",
"You should verify your code in %d min!": "You should verify your code in %d min!",
"the user does not exist, please sign up first": "the user does not exist, please sign up first"
},
"webauthn": {
"Found no credentials for this user": "Found no credentials for this user",
"Please call WebAuthnSigninBegin first": "Please call WebAuthnSigninBegin first"
}
}

142
i18n/locales/pl/data.json Normal file
View File

@ -0,0 +1,142 @@
{
"account": {
"Failed to add user": "Failed to add user",
"Get init score failed, error: %w": "Get init score failed, error: %w",
"Please sign out first": "Please sign out first",
"The application does not allow to sign up new account": "The application does not allow to sign up new account"
},
"auth": {
"Challenge method should be S256": "Challenge method should be S256",
"Failed to create user, user information is invalid: %s": "Failed to create user, user information is invalid: %s",
"Failed to login in: %s": "Failed to login in: %s",
"Invalid token": "Invalid token",
"State expected: %s, but got: %s": "State expected: %s, but got: %s",
"The account for provider: %s and username: %s (%s) does not exist and is not allowed to sign up as new account via %%s, please use another way to sign up": "The account for provider: %s and username: %s (%s) does not exist and is not allowed to sign up as new account via %%s, please use another way to sign up",
"The account for provider: %s and username: %s (%s) does not exist and is not allowed to sign up as new account, please contact your IT support": "The account for provider: %s and username: %s (%s) does not exist and is not allowed to sign up as new account, please contact your IT support",
"The account for provider: %s and username: %s (%s) is already linked to another account: %s (%s)": "The account for provider: %s and username: %s (%s) is already linked to another account: %s (%s)",
"The application: %s does not exist": "The application: %s does not exist",
"The login method: login with password is not enabled for the application": "The login method: login with password is not enabled for the application",
"The provider: %s is not enabled for the application": "The provider: %s is not enabled for the application",
"Unauthorized operation": "Unauthorized operation",
"Unknown authentication type (not password or provider), form = %s": "Unknown authentication type (not password or provider), form = %s",
"User's tag: %s is not listed in the application's tags": "User's tag: %s is not listed in the application's tags"
},
"cas": {
"Service %s and %s do not match": "Service %s and %s do not match"
},
"check": {
"Affiliation cannot be blank": "Affiliation cannot be blank",
"DisplayName cannot be blank": "DisplayName cannot be blank",
"DisplayName is not valid real name": "DisplayName is not valid real name",
"Email already exists": "Email already exists",
"Email cannot be empty": "Email cannot be empty",
"Email is invalid": "Email is invalid",
"Empty username.": "Empty username.",
"FirstName cannot be blank": "FirstName cannot be blank",
"LDAP user name or password incorrect": "LDAP user name or password incorrect",
"LastName cannot be blank": "LastName cannot be blank",
"Multiple accounts with same uid, please check your ldap server": "Multiple accounts with same uid, please check your ldap server",
"Organization does not exist": "Organization does not exist",
"Password must have at least 6 characters": "Password must have at least 6 characters",
"Phone already exists": "Phone already exists",
"Phone cannot be empty": "Phone cannot be empty",
"Phone number is invalid": "Phone number is invalid",
"Session outdated, please login again": "Session outdated, please login again",
"The user is forbidden to sign in, please contact the administrator": "The user is forbidden to sign in, please contact the administrator",
"The user: %s doesn't exist in LDAP server": "The user: %s doesn't exist in LDAP server",
"The username may only contain alphanumeric characters, underlines or hyphens, cannot have consecutive hyphens or underlines, and cannot begin or end with a hyphen or underline.": "The username may only contain alphanumeric characters, underlines or hyphens, cannot have consecutive hyphens or underlines, and cannot begin or end with a hyphen or underline.",
"Username already exists": "Username already exists",
"Username cannot be an email address": "Username cannot be an email address",
"Username cannot contain white spaces": "Username cannot contain white spaces",
"Username cannot start with a digit": "Username cannot start with a digit",
"Username is too long (maximum is 39 characters).": "Username is too long (maximum is 39 characters).",
"Username must have at least 2 characters": "Username must have at least 2 characters",
"You have entered the wrong password or code too many times, please wait for %d minutes and try again": "You have entered the wrong password or code too many times, please wait for %d minutes and try again",
"Your region is not allow to signup by phone": "Your region is not allow to signup by phone",
"password or code is incorrect": "password or code is incorrect",
"password or code is incorrect, you have %d remaining chances": "password or code is incorrect, you have %d remaining chances",
"unsupported password type: %s": "unsupported password type: %s"
},
"general": {
"Missing parameter": "Missing parameter",
"Please login first": "Please login first",
"The user: %s doesn't exist": "The user: %s doesn't exist",
"don't support captchaProvider: ": "don't support captchaProvider: ",
"this operation is not allowed in demo mode": "this operation is not allowed in demo mode"
},
"ldap": {
"Ldap server exist": "Ldap server exist"
},
"link": {
"Please link first": "Please link first",
"This application has no providers": "This application has no providers",
"This application has no providers of type": "This application has no providers of type",
"This provider can't be unlinked": "This provider can't be unlinked",
"You are not the global admin, you can't unlink other users": "You are not the global admin, you can't unlink other users",
"You can't unlink yourself, you are not a member of any application": "You can't unlink yourself, you are not a member of any application"
},
"organization": {
"Only admin can modify the %s.": "Only admin can modify the %s.",
"The %s is immutable.": "The %s is immutable.",
"Unknown modify rule %s.": "Unknown modify rule %s."
},
"provider": {
"Invalid application id": "Invalid application id",
"the provider: %s does not exist": "the provider: %s does not exist"
},
"resource": {
"User is nil for tag: avatar": "User is nil for tag: avatar",
"Username or fullFilePath is empty: username = %s, fullFilePath = %s": "Username or fullFilePath is empty: username = %s, fullFilePath = %s"
},
"saml": {
"Application %s not found": "Application %s not found"
},
"saml_sp": {
"provider %s's category is not SAML": "provider %s's category is not SAML"
},
"service": {
"Empty parameters for emailForm: %v": "Empty parameters for emailForm: %v",
"Invalid Email receivers: %s": "Invalid Email receivers: %s",
"Invalid phone receivers: %s": "Invalid phone receivers: %s"
},
"storage": {
"The objectKey: %s is not allowed": "The objectKey: %s is not allowed",
"The provider type: %s is not supported": "The provider type: %s is not supported"
},
"token": {
"Empty clientId or clientSecret": "Empty clientId or clientSecret",
"Grant_type: %s is not supported in this application": "Grant_type: %s is not supported in this application",
"Invalid application or wrong clientSecret": "Invalid application or wrong clientSecret",
"Invalid client_id": "Invalid client_id",
"Redirect URI: %s doesn't exist in the allowed Redirect URI list": "Redirect URI: %s doesn't exist in the allowed Redirect URI list",
"Token not found, invalid accessToken": "Token not found, invalid accessToken"
},
"user": {
"Display name cannot be empty": "Display name cannot be empty",
"New password cannot contain blank space.": "New password cannot contain blank space."
},
"user_upload": {
"Failed to import users": "Failed to import users"
},
"util": {
"No application is found for userId: %s": "No application is found for userId: %s",
"No provider for category: %s is found for application: %s": "No provider for category: %s is found for application: %s",
"The provider: %s is not found": "The provider: %s is not found"
},
"verification": {
"Code has not been sent yet!": "Code has not been sent yet!",
"Invalid captcha provider.": "Invalid captcha provider.",
"Phone number is invalid in your region %s": "Phone number is invalid in your region %s",
"Turing test failed.": "Turing test failed.",
"Unable to get the email modify rule.": "Unable to get the email modify rule.",
"Unable to get the phone modify rule.": "Unable to get the phone modify rule.",
"Unknown type": "Unknown type",
"Wrong verification code!": "Wrong verification code!",
"You should verify your code in %d min!": "You should verify your code in %d min!",
"the user does not exist, please sign up first": "the user does not exist, please sign up first"
},
"webauthn": {
"Found no credentials for this user": "Found no credentials for this user",
"Please call WebAuthnSigninBegin first": "Please call WebAuthnSigninBegin first"
}
}

142
i18n/locales/sv/data.json Normal file
View File

@ -0,0 +1,142 @@
{
"account": {
"Failed to add user": "Failed to add user",
"Get init score failed, error: %w": "Get init score failed, error: %w",
"Please sign out first": "Please sign out first",
"The application does not allow to sign up new account": "The application does not allow to sign up new account"
},
"auth": {
"Challenge method should be S256": "Challenge method should be S256",
"Failed to create user, user information is invalid: %s": "Failed to create user, user information is invalid: %s",
"Failed to login in: %s": "Failed to login in: %s",
"Invalid token": "Invalid token",
"State expected: %s, but got: %s": "State expected: %s, but got: %s",
"The account for provider: %s and username: %s (%s) does not exist and is not allowed to sign up as new account via %%s, please use another way to sign up": "The account for provider: %s and username: %s (%s) does not exist and is not allowed to sign up as new account via %%s, please use another way to sign up",
"The account for provider: %s and username: %s (%s) does not exist and is not allowed to sign up as new account, please contact your IT support": "The account for provider: %s and username: %s (%s) does not exist and is not allowed to sign up as new account, please contact your IT support",
"The account for provider: %s and username: %s (%s) is already linked to another account: %s (%s)": "The account for provider: %s and username: %s (%s) is already linked to another account: %s (%s)",
"The application: %s does not exist": "The application: %s does not exist",
"The login method: login with password is not enabled for the application": "The login method: login with password is not enabled for the application",
"The provider: %s is not enabled for the application": "The provider: %s is not enabled for the application",
"Unauthorized operation": "Unauthorized operation",
"Unknown authentication type (not password or provider), form = %s": "Unknown authentication type (not password or provider), form = %s",
"User's tag: %s is not listed in the application's tags": "User's tag: %s is not listed in the application's tags"
},
"cas": {
"Service %s and %s do not match": "Service %s and %s do not match"
},
"check": {
"Affiliation cannot be blank": "Affiliation cannot be blank",
"DisplayName cannot be blank": "DisplayName cannot be blank",
"DisplayName is not valid real name": "DisplayName is not valid real name",
"Email already exists": "Email already exists",
"Email cannot be empty": "Email cannot be empty",
"Email is invalid": "Email is invalid",
"Empty username.": "Empty username.",
"FirstName cannot be blank": "FirstName cannot be blank",
"LDAP user name or password incorrect": "LDAP user name or password incorrect",
"LastName cannot be blank": "LastName cannot be blank",
"Multiple accounts with same uid, please check your ldap server": "Multiple accounts with same uid, please check your ldap server",
"Organization does not exist": "Organization does not exist",
"Password must have at least 6 characters": "Password must have at least 6 characters",
"Phone already exists": "Phone already exists",
"Phone cannot be empty": "Phone cannot be empty",
"Phone number is invalid": "Phone number is invalid",
"Session outdated, please login again": "Session outdated, please login again",
"The user is forbidden to sign in, please contact the administrator": "The user is forbidden to sign in, please contact the administrator",
"The user: %s doesn't exist in LDAP server": "The user: %s doesn't exist in LDAP server",
"The username may only contain alphanumeric characters, underlines or hyphens, cannot have consecutive hyphens or underlines, and cannot begin or end with a hyphen or underline.": "The username may only contain alphanumeric characters, underlines or hyphens, cannot have consecutive hyphens or underlines, and cannot begin or end with a hyphen or underline.",
"Username already exists": "Username already exists",
"Username cannot be an email address": "Username cannot be an email address",
"Username cannot contain white spaces": "Username cannot contain white spaces",
"Username cannot start with a digit": "Username cannot start with a digit",
"Username is too long (maximum is 39 characters).": "Username is too long (maximum is 39 characters).",
"Username must have at least 2 characters": "Username must have at least 2 characters",
"You have entered the wrong password or code too many times, please wait for %d minutes and try again": "You have entered the wrong password or code too many times, please wait for %d minutes and try again",
"Your region is not allow to signup by phone": "Your region is not allow to signup by phone",
"password or code is incorrect": "password or code is incorrect",
"password or code is incorrect, you have %d remaining chances": "password or code is incorrect, you have %d remaining chances",
"unsupported password type: %s": "unsupported password type: %s"
},
"general": {
"Missing parameter": "Missing parameter",
"Please login first": "Please login first",
"The user: %s doesn't exist": "The user: %s doesn't exist",
"don't support captchaProvider: ": "don't support captchaProvider: ",
"this operation is not allowed in demo mode": "this operation is not allowed in demo mode"
},
"ldap": {
"Ldap server exist": "Ldap server exist"
},
"link": {
"Please link first": "Please link first",
"This application has no providers": "This application has no providers",
"This application has no providers of type": "This application has no providers of type",
"This provider can't be unlinked": "This provider can't be unlinked",
"You are not the global admin, you can't unlink other users": "You are not the global admin, you can't unlink other users",
"You can't unlink yourself, you are not a member of any application": "You can't unlink yourself, you are not a member of any application"
},
"organization": {
"Only admin can modify the %s.": "Only admin can modify the %s.",
"The %s is immutable.": "The %s is immutable.",
"Unknown modify rule %s.": "Unknown modify rule %s."
},
"provider": {
"Invalid application id": "Invalid application id",
"the provider: %s does not exist": "the provider: %s does not exist"
},
"resource": {
"User is nil for tag: avatar": "User is nil for tag: avatar",
"Username or fullFilePath is empty: username = %s, fullFilePath = %s": "Username or fullFilePath is empty: username = %s, fullFilePath = %s"
},
"saml": {
"Application %s not found": "Application %s not found"
},
"saml_sp": {
"provider %s's category is not SAML": "provider %s's category is not SAML"
},
"service": {
"Empty parameters for emailForm: %v": "Empty parameters for emailForm: %v",
"Invalid Email receivers: %s": "Invalid Email receivers: %s",
"Invalid phone receivers: %s": "Invalid phone receivers: %s"
},
"storage": {
"The objectKey: %s is not allowed": "The objectKey: %s is not allowed",
"The provider type: %s is not supported": "The provider type: %s is not supported"
},
"token": {
"Empty clientId or clientSecret": "Empty clientId or clientSecret",
"Grant_type: %s is not supported in this application": "Grant_type: %s is not supported in this application",
"Invalid application or wrong clientSecret": "Invalid application or wrong clientSecret",
"Invalid client_id": "Invalid client_id",
"Redirect URI: %s doesn't exist in the allowed Redirect URI list": "Redirect URI: %s doesn't exist in the allowed Redirect URI list",
"Token not found, invalid accessToken": "Token not found, invalid accessToken"
},
"user": {
"Display name cannot be empty": "Display name cannot be empty",
"New password cannot contain blank space.": "New password cannot contain blank space."
},
"user_upload": {
"Failed to import users": "Failed to import users"
},
"util": {
"No application is found for userId: %s": "No application is found for userId: %s",
"No provider for category: %s is found for application: %s": "No provider for category: %s is found for application: %s",
"The provider: %s is not found": "The provider: %s is not found"
},
"verification": {
"Code has not been sent yet!": "Code has not been sent yet!",
"Invalid captcha provider.": "Invalid captcha provider.",
"Phone number is invalid in your region %s": "Phone number is invalid in your region %s",
"Turing test failed.": "Turing test failed.",
"Unable to get the email modify rule.": "Unable to get the email modify rule.",
"Unable to get the phone modify rule.": "Unable to get the phone modify rule.",
"Unknown type": "Unknown type",
"Wrong verification code!": "Wrong verification code!",
"You should verify your code in %d min!": "You should verify your code in %d min!",
"the user does not exist, please sign up first": "the user does not exist, please sign up first"
},
"webauthn": {
"Found no credentials for this user": "Found no credentials for this user",
"Please call WebAuthnSigninBegin first": "Please call WebAuthnSigninBegin first"
}
}

142
i18n/locales/uk/data.json Normal file
View File

@ -0,0 +1,142 @@
{
"account": {
"Failed to add user": "Failed to add user",
"Get init score failed, error: %w": "Get init score failed, error: %w",
"Please sign out first": "Please sign out first",
"The application does not allow to sign up new account": "The application does not allow to sign up new account"
},
"auth": {
"Challenge method should be S256": "Challenge method should be S256",
"Failed to create user, user information is invalid: %s": "Failed to create user, user information is invalid: %s",
"Failed to login in: %s": "Failed to login in: %s",
"Invalid token": "Invalid token",
"State expected: %s, but got: %s": "State expected: %s, but got: %s",
"The account for provider: %s and username: %s (%s) does not exist and is not allowed to sign up as new account via %%s, please use another way to sign up": "The account for provider: %s and username: %s (%s) does not exist and is not allowed to sign up as new account via %%s, please use another way to sign up",
"The account for provider: %s and username: %s (%s) does not exist and is not allowed to sign up as new account, please contact your IT support": "The account for provider: %s and username: %s (%s) does not exist and is not allowed to sign up as new account, please contact your IT support",
"The account for provider: %s and username: %s (%s) is already linked to another account: %s (%s)": "The account for provider: %s and username: %s (%s) is already linked to another account: %s (%s)",
"The application: %s does not exist": "The application: %s does not exist",
"The login method: login with password is not enabled for the application": "The login method: login with password is not enabled for the application",
"The provider: %s is not enabled for the application": "The provider: %s is not enabled for the application",
"Unauthorized operation": "Unauthorized operation",
"Unknown authentication type (not password or provider), form = %s": "Unknown authentication type (not password or provider), form = %s",
"User's tag: %s is not listed in the application's tags": "User's tag: %s is not listed in the application's tags"
},
"cas": {
"Service %s and %s do not match": "Service %s and %s do not match"
},
"check": {
"Affiliation cannot be blank": "Affiliation cannot be blank",
"DisplayName cannot be blank": "DisplayName cannot be blank",
"DisplayName is not valid real name": "DisplayName is not valid real name",
"Email already exists": "Email already exists",
"Email cannot be empty": "Email cannot be empty",
"Email is invalid": "Email is invalid",
"Empty username.": "Empty username.",
"FirstName cannot be blank": "FirstName cannot be blank",
"LDAP user name or password incorrect": "LDAP user name or password incorrect",
"LastName cannot be blank": "LastName cannot be blank",
"Multiple accounts with same uid, please check your ldap server": "Multiple accounts with same uid, please check your ldap server",
"Organization does not exist": "Organization does not exist",
"Password must have at least 6 characters": "Password must have at least 6 characters",
"Phone already exists": "Phone already exists",
"Phone cannot be empty": "Phone cannot be empty",
"Phone number is invalid": "Phone number is invalid",
"Session outdated, please login again": "Session outdated, please login again",
"The user is forbidden to sign in, please contact the administrator": "The user is forbidden to sign in, please contact the administrator",
"The user: %s doesn't exist in LDAP server": "The user: %s doesn't exist in LDAP server",
"The username may only contain alphanumeric characters, underlines or hyphens, cannot have consecutive hyphens or underlines, and cannot begin or end with a hyphen or underline.": "The username may only contain alphanumeric characters, underlines or hyphens, cannot have consecutive hyphens or underlines, and cannot begin or end with a hyphen or underline.",
"Username already exists": "Username already exists",
"Username cannot be an email address": "Username cannot be an email address",
"Username cannot contain white spaces": "Username cannot contain white spaces",
"Username cannot start with a digit": "Username cannot start with a digit",
"Username is too long (maximum is 39 characters).": "Username is too long (maximum is 39 characters).",
"Username must have at least 2 characters": "Username must have at least 2 characters",
"You have entered the wrong password or code too many times, please wait for %d minutes and try again": "You have entered the wrong password or code too many times, please wait for %d minutes and try again",
"Your region is not allow to signup by phone": "Your region is not allow to signup by phone",
"password or code is incorrect": "password or code is incorrect",
"password or code is incorrect, you have %d remaining chances": "password or code is incorrect, you have %d remaining chances",
"unsupported password type: %s": "unsupported password type: %s"
},
"general": {
"Missing parameter": "Missing parameter",
"Please login first": "Please login first",
"The user: %s doesn't exist": "The user: %s doesn't exist",
"don't support captchaProvider: ": "don't support captchaProvider: ",
"this operation is not allowed in demo mode": "this operation is not allowed in demo mode"
},
"ldap": {
"Ldap server exist": "Ldap server exist"
},
"link": {
"Please link first": "Please link first",
"This application has no providers": "This application has no providers",
"This application has no providers of type": "This application has no providers of type",
"This provider can't be unlinked": "This provider can't be unlinked",
"You are not the global admin, you can't unlink other users": "You are not the global admin, you can't unlink other users",
"You can't unlink yourself, you are not a member of any application": "You can't unlink yourself, you are not a member of any application"
},
"organization": {
"Only admin can modify the %s.": "Only admin can modify the %s.",
"The %s is immutable.": "The %s is immutable.",
"Unknown modify rule %s.": "Unknown modify rule %s."
},
"provider": {
"Invalid application id": "Invalid application id",
"the provider: %s does not exist": "the provider: %s does not exist"
},
"resource": {
"User is nil for tag: avatar": "User is nil for tag: avatar",
"Username or fullFilePath is empty: username = %s, fullFilePath = %s": "Username or fullFilePath is empty: username = %s, fullFilePath = %s"
},
"saml": {
"Application %s not found": "Application %s not found"
},
"saml_sp": {
"provider %s's category is not SAML": "provider %s's category is not SAML"
},
"service": {
"Empty parameters for emailForm: %v": "Empty parameters for emailForm: %v",
"Invalid Email receivers: %s": "Invalid Email receivers: %s",
"Invalid phone receivers: %s": "Invalid phone receivers: %s"
},
"storage": {
"The objectKey: %s is not allowed": "The objectKey: %s is not allowed",
"The provider type: %s is not supported": "The provider type: %s is not supported"
},
"token": {
"Empty clientId or clientSecret": "Empty clientId or clientSecret",
"Grant_type: %s is not supported in this application": "Grant_type: %s is not supported in this application",
"Invalid application or wrong clientSecret": "Invalid application or wrong clientSecret",
"Invalid client_id": "Invalid client_id",
"Redirect URI: %s doesn't exist in the allowed Redirect URI list": "Redirect URI: %s doesn't exist in the allowed Redirect URI list",
"Token not found, invalid accessToken": "Token not found, invalid accessToken"
},
"user": {
"Display name cannot be empty": "Display name cannot be empty",
"New password cannot contain blank space.": "New password cannot contain blank space."
},
"user_upload": {
"Failed to import users": "Failed to import users"
},
"util": {
"No application is found for userId: %s": "No application is found for userId: %s",
"No provider for category: %s is found for application: %s": "No provider for category: %s is found for application: %s",
"The provider: %s is not found": "The provider: %s is not found"
},
"verification": {
"Code has not been sent yet!": "Code has not been sent yet!",
"Invalid captcha provider.": "Invalid captcha provider.",
"Phone number is invalid in your region %s": "Phone number is invalid in your region %s",
"Turing test failed.": "Turing test failed.",
"Unable to get the email modify rule.": "Unable to get the email modify rule.",
"Unable to get the phone modify rule.": "Unable to get the phone modify rule.",
"Unknown type": "Unknown type",
"Wrong verification code!": "Wrong verification code!",
"You should verify your code in %d min!": "You should verify your code in %d min!",
"the user does not exist, please sign up first": "the user does not exist, please sign up first"
},
"webauthn": {
"Found no credentials for this user": "Found no credentials for this user",
"Please call WebAuthnSigninBegin first": "Please call WebAuthnSigninBegin first"
}
}

View File

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

View File

@ -9,11 +9,11 @@
"passwordType": "plain", "passwordType": "plain",
"passwordSalt": "", "passwordSalt": "",
"passwordOptions": ["AtLeast6"], "passwordOptions": ["AtLeast6"],
"countryCodes": ["US", "ES", "CN", "FR", "DE", "GB", "JP", "KR", "VN", "ID", "SG", "IN", "IT", "MY", "TR", "DZ", "IL", "PH"], "countryCodes": ["US", "GB", "ES", "FR", "DE", "CN", "JP", "KR", "VN", "ID", "SG", "IN", "IT", "MY", "TR", "DZ", "IL", "PH", "NL", "PL", "FI", "SE", "UA", "KZ"],
"defaultAvatar": "", "defaultAvatar": "",
"defaultApplication": "", "defaultApplication": "",
"tags": [], "tags": [],
"languages": ["en", "zh", "es", "fr", "de", "id", "ja", "ko", "ru", "vi", "it", "ms", "tr","ar", "he", "fi"], "languages": ["en", "zh", "es", "fr", "de", "id", "ja", "ko", "ru", "vi", "it", "ms", "tr","ar", "he", "nl", "pl", "fi", "sv", "uk", "kk", "fa"],
"masterPassword": "", "masterPassword": "",
"initScore": 2000, "initScore": 2000,
"enableSoftDeletion": false, "enableSoftDeletion": false,

29
notification/bark.go Normal file
View File

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

33
notification/dingtalk.go Normal file
View File

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

37
notification/discord.go Normal file
View File

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

View File

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

29
notification/lark.go Normal file
View File

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

32
notification/line.go Normal file
View File

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

36
notification/matrix.go Normal file
View File

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

View File

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

View File

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

View File

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

31
notification/pushover.go Normal file
View File

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

34
notification/reddit.go Normal file
View File

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

View File

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

30
notification/slack.go Normal file
View File

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

View File

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

41
notification/twitter.go Normal file
View File

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

36
notification/viber.go Normal file
View File

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

33
notification/webpush.go Normal file
View File

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

View File

@ -151,7 +151,7 @@ func (adapter *Adapter) InitAdapter() error {
if adapter.Adapter == nil { if adapter.Adapter == nil {
var dataSourceName string var dataSourceName string
if adapter.builtInAdapter() { if adapter.isBuiltIn() {
dataSourceName = conf.GetConfigString("dataSourceName") dataSourceName = conf.GetConfigString("dataSourceName")
if adapter.DatabaseType == "mysql" { if adapter.DatabaseType == "mysql" {
dataSourceName = dataSourceName + adapter.Database dataSourceName = dataSourceName + adapter.Database
@ -183,6 +183,14 @@ func (adapter *Adapter) InitAdapter() error {
var err error var err error
engine, err := xorm.NewEngine(adapter.DatabaseType, dataSourceName) engine, err := xorm.NewEngine(adapter.DatabaseType, dataSourceName)
if adapter.isBuiltIn() && adapter.DatabaseType == "postgres" {
schema := util.GetValueFromDataSourceName("search_path", dataSourceName)
if schema != "" {
engine.SetSchema(schema)
}
}
adapter.Adapter, err = xormadapter.NewAdapterByEngineWithTableName(engine, adapter.getTable(), adapter.TableNamePrefix) adapter.Adapter, err = xormadapter.NewAdapterByEngineWithTableName(engine, adapter.getTable(), adapter.TableNamePrefix)
if err != nil { if err != nil {
return err return err
@ -211,7 +219,7 @@ func adapterChangeTrigger(oldName string, newName string) error {
return session.Commit() return session.Commit()
} }
func (adapter *Adapter) builtInAdapter() bool { func (adapter *Adapter) isBuiltIn() bool {
if adapter.Owner != "built-in" { if adapter.Owner != "built-in" {
return false return false
} }

View File

@ -57,6 +57,7 @@ type Application struct {
SignupItems []*SignupItem `xorm:"varchar(1000)" json:"signupItems"` SignupItems []*SignupItem `xorm:"varchar(1000)" json:"signupItems"`
GrantTypes []string `xorm:"varchar(1000)" json:"grantTypes"` GrantTypes []string `xorm:"varchar(1000)" json:"grantTypes"`
OrganizationObj *Organization `xorm:"-" json:"organizationObj"` OrganizationObj *Organization `xorm:"-" json:"organizationObj"`
CertPublicKey string `xorm:"-" json:"certPublicKey"`
Tags []string `xorm:"mediumtext" json:"tags"` Tags []string `xorm:"mediumtext" json:"tags"`
InvitationCodes []string `xorm:"varchar(200)" json:"invitationCodes"` InvitationCodes []string `xorm:"varchar(200)" json:"invitationCodes"`
@ -427,15 +428,14 @@ func (application *Application) GetId() string {
} }
func (application *Application) IsRedirectUriValid(redirectUri string) bool { func (application *Application) IsRedirectUriValid(redirectUri string) bool {
isValid := false redirectUris := append([]string{"http://localhost:"}, application.RedirectUris...)
for _, targetUri := range application.RedirectUris { for _, targetUri := range redirectUris {
targetUriRegex := regexp.MustCompile(targetUri) targetUriRegex := regexp.MustCompile(targetUri)
if targetUriRegex.MatchString(redirectUri) || strings.Contains(redirectUri, targetUri) { if targetUriRegex.MatchString(redirectUri) || strings.Contains(redirectUri, targetUri) {
isValid = true return true
break
} }
} }
return isValid return false
} }
func IsOriginAllowed(origin string) (bool, error) { func IsOriginAllowed(origin string) (bool, error) {

View File

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

BIN
object/cert.go~ Normal file

Binary file not shown.

View File

@ -350,7 +350,7 @@ func CheckUserPermission(requestUserId, userId string, strict bool, lang string)
return hasPermission, fmt.Errorf(i18n.Translate(lang, "auth:Unauthorized operation")) return hasPermission, fmt.Errorf(i18n.Translate(lang, "auth:Unauthorized operation"))
} }
func CheckAccessPermission(userId string, application *Application) (bool, error) { func CheckLoginPermission(userId string, application *Application) (bool, error) {
var err error var err error
if userId == "built-in/admin" { if userId == "built-in/admin" {
return true, nil return true, nil
@ -361,32 +361,40 @@ func CheckAccessPermission(userId string, application *Application) (bool, error
return false, err return false, err
} }
allowed := true allowCount := 0
denyCount := 0
for _, permission := range permissions { for _, permission := range permissions {
if !permission.IsEnabled { if !permission.IsEnabled || permission.ResourceType != "Application" || !permission.isResourceHit(application.Name) {
continue continue
} }
isHit := false if permission.isUserHit(userId) {
for _, resource := range permission.Resources { allowCount += 1
if application.Name == resource {
isHit = true
break
}
} }
if isHit { enforcer := getPermissionEnforcer(permission)
containsAsterisk := ContainsAsterisk(userId, permission.Users)
if containsAsterisk { var isAllowed bool
return true, err isAllowed, err = enforcer.Enforce(userId, application.Name, "Read")
if err != nil {
return false, err
}
if isAllowed {
if permission.Effect == "Allow" {
allowCount += 1
} }
enforcer := getPermissionEnforcer(permission) } else {
if allowed, err = enforcer.Enforce(userId, application.Name, "read"); allowed { if permission.Effect == "Deny" {
return allowed, err denyCount += 1
} }
} }
} }
return allowed, err
if denyCount > 0 {
return false, nil
}
return true, nil
} }
func CheckUsername(username string, lang string) string { func CheckUsername(username string, lang string) string {
@ -406,10 +414,6 @@ func CheckUsername(username string, lang string) string {
} }
func CheckUpdateUser(oldUser, user *User, lang string) string { func CheckUpdateUser(oldUser, user *User, lang string) string {
if user.DisplayName == "" {
return i18n.Translate(lang, "user:Display name cannot be empty")
}
if oldUser.Name != user.Name { if oldUser.Name != user.Name {
if msg := CheckUsername(user.Name, lang); msg != "" { if msg := CheckUsername(user.Name, lang); msg != "" {
return msg return msg
@ -419,7 +423,7 @@ func CheckUpdateUser(oldUser, user *User, lang string) string {
} }
} }
if oldUser.Email != user.Email { if oldUser.Email != user.Email {
if HasUserByField(user.Name, "email", user.Email) { if HasUserByField(user.Owner, "email", user.Email) {
return i18n.Translate(lang, "check:Email already exists") return i18n.Translate(lang, "check:Email already exists")
} }
} }

View File

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

View File

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

View File

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

View File

@ -18,11 +18,14 @@ import (
"database/sql" "database/sql"
"flag" "flag"
"fmt" "fmt"
"os"
"regexp"
"runtime" "runtime"
"strings" "strings"
"github.com/beego/beego" "github.com/beego/beego"
"github.com/casdoor/casdoor/conf" "github.com/casdoor/casdoor/conf"
"github.com/casdoor/casdoor/util"
xormadapter "github.com/casdoor/xorm-adapter/v3" xormadapter "github.com/casdoor/xorm-adapter/v3"
_ "github.com/denisenkom/go-mssqldb" // db = mssql _ "github.com/denisenkom/go-mssqldb" // db = mssql
_ "github.com/go-sql-driver/mysql" // db = mysql _ "github.com/go-sql-driver/mysql" // db = mysql
@ -65,6 +68,17 @@ func InitConfig() {
} }
func InitAdapter() { func InitAdapter() {
if conf.GetConfigString("driverName") == "" {
if !util.FileExist("conf/app.conf") {
dir, err := os.Getwd()
if err != nil {
panic(err)
}
dir = strings.ReplaceAll(dir, "\\", "/")
panic(fmt.Sprintf("The Casdoor config file: \"app.conf\" was not found, it should be placed at: \"%s/conf/app.conf\"", dir))
}
}
if createDatabase { if createDatabase {
err := createDatabaseForPostgres(conf.GetConfigString("driverName"), conf.GetConfigDataSourceName(), conf.GetConfigString("dbName")) err := createDatabaseForPostgres(conf.GetConfigString("driverName"), conf.GetConfigDataSourceName(), conf.GetConfigString("dbName"))
if err != nil { if err != nil {
@ -122,9 +136,14 @@ func NewAdapter(driverName string, dataSourceName string, dbName string) *Ormer
return a return a
} }
func refineDataSourceNameForPostgres(dataSourceName string) string {
reg := regexp.MustCompile(`dbname=[^ ]+\s*`)
return reg.ReplaceAllString(dataSourceName, "")
}
func createDatabaseForPostgres(driverName string, dataSourceName string, dbName string) error { func createDatabaseForPostgres(driverName string, dataSourceName string, dbName string) error {
if driverName == "postgres" { if driverName == "postgres" {
db, err := sql.Open(driverName, dataSourceName) db, err := sql.Open(driverName, refineDataSourceNameForPostgres(dataSourceName))
if err != nil { if err != nil {
return err return err
} }
@ -136,6 +155,21 @@ func createDatabaseForPostgres(driverName string, dataSourceName string, dbName
return err return err
} }
} }
schema := util.GetValueFromDataSourceName("search_path", dataSourceName)
if schema != "" {
db, err = sql.Open(driverName, dataSourceName)
if err != nil {
return err
}
defer db.Close()
_, err = db.Exec(fmt.Sprintf("CREATE SCHEMA %s;", schema))
if err != nil {
if !strings.Contains(err.Error(), "already exists") {
return err
}
}
}
return nil return nil
} else { } else {
@ -168,6 +202,12 @@ func (a *Ormer) open() {
if err != nil { if err != nil {
panic(err) panic(err)
} }
if a.driverName == "postgres" {
schema := util.GetValueFromDataSourceName("search_path", dataSourceName)
if schema != "" {
engine.SetSchema(schema)
}
}
a.Engine = engine a.Engine = engine
} }

View File

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

View File

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

View File

@ -79,9 +79,7 @@ func (p *Permission) setEnforcerAdapter(enforcer *casbin.Enforcer) error {
} }
} }
tableNamePrefix := conf.GetConfigString("tableNamePrefix") tableNamePrefix := conf.GetConfigString("tableNamePrefix")
driverName := conf.GetConfigString("driverName") adapter, err := xormadapter.NewAdapterByEngineWithTableName(ormer.Engine, tableName, tableNamePrefix)
dataSourceName := conf.GetConfigRealDataSourceName(driverName)
adapter, err := xormadapter.NewAdapterWithTableName(driverName, dataSourceName, tableName, tableNamePrefix, true)
if err != nil { if err != nil {
return err return err
} }

View File

@ -16,6 +16,7 @@ package object
import ( import (
"fmt" "fmt"
"time"
"github.com/casdoor/casdoor/util" "github.com/casdoor/casdoor/util"
"github.com/xorm-io/core" "github.com/xorm-io/core"
@ -28,10 +29,10 @@ type Plan struct {
DisplayName string `xorm:"varchar(100)" json:"displayName"` DisplayName string `xorm:"varchar(100)" json:"displayName"`
Description string `xorm:"varchar(100)" json:"description"` Description string `xorm:"varchar(100)" json:"description"`
PricePerMonth float64 `json:"pricePerMonth"` Price float64 `json:"price"`
PricePerYear float64 `json:"pricePerYear"`
Currency string `xorm:"varchar(100)" json:"currency"` Currency string `xorm:"varchar(100)" json:"currency"`
Product string `json:"product"` // related product id Period string `xorm:"varchar(100)" json:"period"`
Product string `xorm:"varchar(100)" json:"product"`
PaymentProviders []string `xorm:"varchar(100)" json:"paymentProviders"` // payment providers for related product PaymentProviders []string `xorm:"varchar(100)" json:"paymentProviders"` // payment providers for related product
IsEnabled bool `json:"isEnabled"` IsEnabled bool `json:"isEnabled"`
@ -39,10 +40,28 @@ type Plan struct {
Options []string `xorm:"-" json:"options"` Options []string `xorm:"-" json:"options"`
} }
const (
PeriodMonthly = "Monthly"
PeriodYearly = "Yearly"
)
func (plan *Plan) GetId() string { func (plan *Plan) GetId() string {
return fmt.Sprintf("%s/%s", plan.Owner, plan.Name) return fmt.Sprintf("%s/%s", plan.Owner, plan.Name)
} }
func GetDuration(period string) (startTime time.Time, endTime time.Time) {
if period == PeriodYearly {
startTime = time.Now()
endTime = startTime.AddDate(1, 0, 0)
} else if period == PeriodMonthly {
startTime = time.Now()
endTime = startTime.AddDate(0, 1, 0)
} else {
panic(fmt.Sprintf("invalid period: %s", period))
}
return
}
func GetPlanCount(owner, field, value string) (int64, error) { func GetPlanCount(owner, field, value string) (int64, error) {
session := GetSession(owner, -1, -1, field, value, "", "") session := GetSession(owner, -1, -1, field, value, "", "")
return session.Count(&Plan{}) return session.Count(&Plan{})

View File

@ -32,12 +32,6 @@ type Pricing struct {
IsEnabled bool `json:"isEnabled"` IsEnabled bool `json:"isEnabled"`
TrialDuration int `json:"trialDuration"` TrialDuration int `json:"trialDuration"`
Application string `xorm:"varchar(100)" json:"application"` Application string `xorm:"varchar(100)" json:"application"`
Submitter string `xorm:"varchar(100)" json:"submitter"`
Approver string `xorm:"varchar(100)" json:"approver"`
ApproveTime string `xorm:"varchar(100)" json:"approveTime"`
State string `xorm:"varchar(100)" json:"state"`
} }
func (pricing *Pricing) GetId() string { func (pricing *Pricing) GetId() string {

View File

@ -37,7 +37,7 @@ type Product struct {
Price float64 `json:"price"` Price float64 `json:"price"`
Quantity int `json:"quantity"` Quantity int `json:"quantity"`
Sold int `json:"sold"` Sold int `json:"sold"`
Providers []string `xorm:"varchar(100)" json:"providers"` Providers []string `xorm:"varchar(255)" json:"providers"`
ReturnUrl string `xorm:"varchar(1000)" json:"returnUrl"` ReturnUrl string `xorm:"varchar(1000)" json:"returnUrl"`
State string `xorm:"varchar(100)" json:"state"` State string `xorm:"varchar(100)" json:"state"`
@ -158,24 +158,23 @@ func (product *Product) getProvider(providerName string) (*Provider, error) {
return provider, nil return provider, nil
} }
func BuyProduct(id string, user *User, providerName, pricingName, planName, host string) (string, string, error) { func BuyProduct(id string, user *User, providerName, pricingName, planName, host string) (*Payment, error) {
product, err := GetProduct(id) product, err := GetProduct(id)
if err != nil { if err != nil {
return "", "", err return nil, err
} }
if product == nil { if product == nil {
return "", "", fmt.Errorf("the product: %s does not exist", id) return nil, fmt.Errorf("the product: %s does not exist", id)
} }
provider, err := product.getProvider(providerName) provider, err := product.getProvider(providerName)
if err != nil { if err != nil {
return "", "", err return nil, err
} }
pProvider, _, err := provider.getPaymentProvider() pProvider, err := GetPaymentProvider(provider)
if err != nil { if err != nil {
return "", "", err return nil, err
} }
owner := product.Owner owner := product.Owner
@ -190,10 +189,17 @@ func BuyProduct(id string, user *User, providerName, pricingName, planName, host
if user.Type == "paid-user" { if user.Type == "paid-user" {
// Create a subscription for `paid-user` // Create a subscription for `paid-user`
if pricingName != "" && planName != "" { if pricingName != "" && planName != "" {
sub := NewSubscription(owner, user.Name, pricingName, planName, paymentName) plan, err := GetPlan(util.GetId(owner, planName))
_, err := AddSubscription(sub)
if err != nil { if err != nil {
return "", "", err return nil, err
}
if plan == nil {
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 nil, err
} }
returnUrl = fmt.Sprintf("%s/buy-plan/%s/%s/result?subscription=%s", originFrontend, owner, pricingName, sub.Name) returnUrl = fmt.Sprintf("%s/buy-plan/%s/%s/result?subscription=%s", originFrontend, owner, pricingName, sub.Name)
} }
@ -201,10 +207,10 @@ func BuyProduct(id string, user *User, providerName, pricingName, planName, host
// Create an OrderId and get the payUrl // Create an OrderId and get the payUrl
payUrl, orderId, err := pProvider.Pay(providerName, productName, payerName, paymentName, productDisplayName, product.Price, product.Currency, returnUrl, notifyUrl) payUrl, orderId, err := pProvider.Pay(providerName, productName, payerName, paymentName, productDisplayName, product.Price, product.Currency, returnUrl, notifyUrl)
if err != nil { if err != nil {
return "", "", err return nil, err
} }
// Create a Payment linked with Product and Order // Create a Payment linked with Product and Order
payment := Payment{ payment := &Payment{
Owner: product.Owner, Owner: product.Owner,
Name: paymentName, Name: paymentName,
CreatedTime: util.GetCurrentTime(), CreatedTime: util.GetCurrentTime(),
@ -223,6 +229,7 @@ func BuyProduct(id string, user *User, providerName, pricingName, planName, host
User: user.Name, User: user.Name,
PayUrl: payUrl, PayUrl: payUrl,
SuccessUrl: returnUrl,
State: pp.PaymentStateCreated, State: pp.PaymentStateCreated,
OutOrderId: orderId, OutOrderId: orderId,
} }
@ -231,15 +238,15 @@ func BuyProduct(id string, user *User, providerName, pricingName, planName, host
payment.State = pp.PaymentStatePaid payment.State = pp.PaymentStatePaid
} }
affected, err := AddPayment(&payment) affected, err := AddPayment(payment)
if err != nil { if err != nil {
return "", "", err return nil, err
} }
if !affected { if !affected {
return "", "", fmt.Errorf("failed to add payment: %s", util.StructToJson(payment)) return nil, fmt.Errorf("failed to add payment: %s", util.StructToJson(payment))
} }
return payUrl, orderId, err return payment, err
} }
func ExtendProductWithProviders(product *Product) error { func ExtendProductWithProviders(product *Product) error {
@ -267,14 +274,14 @@ func CreateProductForPlan(plan *Plan) *Product {
product := &Product{ product := &Product{
Owner: plan.Owner, Owner: plan.Owner,
Name: fmt.Sprintf("product_%v", util.GetRandomName()), Name: fmt.Sprintf("product_%v", util.GetRandomName()),
DisplayName: fmt.Sprintf("Auto Created Product for Plan %v(%v)", plan.GetId(), plan.DisplayName), DisplayName: fmt.Sprintf("Product for Plan %v/%v/%v", plan.Name, plan.DisplayName, plan.Period),
CreatedTime: plan.CreatedTime, CreatedTime: plan.CreatedTime,
Image: "https://cdn.casbin.org/img/casdoor-logo_1185x256.png", // TODO Image: "https://cdn.casbin.org/img/casdoor-logo_1185x256.png", // TODO
Detail: fmt.Sprintf("This Product was auto created for Plan %v(%v)", plan.GetId(), plan.DisplayName), Detail: fmt.Sprintf("This product was auto created for plan %v(%v), subscription period is %v", plan.Name, plan.DisplayName, plan.Period),
Description: plan.Description, Description: plan.Description,
Tag: "auto_created_product_for_plan", Tag: "auto_created_product_for_plan",
Price: plan.PricePerMonth, // TODO Price: plan.Price,
Currency: plan.Currency, Currency: plan.Currency,
Quantity: 999, Quantity: 999,
@ -290,9 +297,10 @@ func CreateProductForPlan(plan *Plan) *Product {
} }
func UpdateProductForPlan(plan *Plan, product *Product) { func UpdateProductForPlan(plan *Plan, product *Product) {
product.DisplayName = fmt.Sprintf("Auto Created Product for Plan %v(%v)", plan.GetId(), plan.DisplayName) product.Owner = plan.Owner
product.Detail = fmt.Sprintf("This Product was auto created for Plan %v(%v)", plan.GetId(), plan.DisplayName) product.DisplayName = fmt.Sprintf("Product for Plan %v/%v/%v", plan.Name, plan.DisplayName, plan.Period)
product.Price = plan.PricePerMonth // TODO product.Detail = fmt.Sprintf("This product was auto created for plan %v(%v), subscription period is %v", plan.Name, plan.DisplayName, plan.Period)
product.Providers = plan.PaymentProviders product.Price = plan.Price
product.Currency = plan.Currency product.Currency = plan.Currency
product.Providers = plan.PaymentProviders
} }

View File

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

View File

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

View File

@ -50,7 +50,7 @@ type Subscription struct {
StartTime time.Time `json:"startTime"` StartTime time.Time `json:"startTime"`
EndTime time.Time `json:"endTime"` EndTime time.Time `json:"endTime"`
Duration int `json:"duration"` Period string `xorm:"varchar(100)" json:"period"`
State SubscriptionState `xorm:"varchar(100)" json:"state"` State SubscriptionState `xorm:"varchar(100)" json:"state"`
} }
@ -103,7 +103,8 @@ func (sub *Subscription) UpdateState() error {
return nil return nil
} }
func NewSubscription(owner, userName, pricingName, planName, paymentName string) *Subscription { func NewSubscription(owner, userName, planName, paymentName, period string) *Subscription {
startTime, endTime := GetDuration(period)
id := util.GenerateId()[:6] id := util.GenerateId()[:6]
return &Subscription{ return &Subscription{
Owner: owner, Owner: owner,
@ -112,13 +113,12 @@ func NewSubscription(owner, userName, pricingName, planName, paymentName string)
CreatedTime: util.GetCurrentTime(), CreatedTime: util.GetCurrentTime(),
User: userName, User: userName,
Pricing: pricingName,
Plan: planName, Plan: planName,
Payment: paymentName, Payment: paymentName,
StartTime: time.Now(), StartTime: startTime,
EndTime: time.Now().AddDate(0, 0, 30), EndTime: endTime,
Duration: 30, // TODO Period: period,
State: SubStatePending, // waiting for payment complete State: SubStatePending, // waiting for payment complete
} }
} }

View File

@ -148,6 +148,6 @@ func (syncer *Syncer) syncUsers() error {
func (syncer *Syncer) syncUsersNoError() { func (syncer *Syncer) syncUsersNoError() {
err := syncer.syncUsers() err := syncer.syncUsers()
if err != nil { if err != nil {
panic(err) fmt.Printf("syncUsersNoError() error: %s\n", err.Error())
} }
} }

View File

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

View File

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

View File

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

View File

@ -194,16 +194,17 @@ type User struct {
} }
type Userinfo struct { type Userinfo struct {
Sub string `json:"sub"` Sub string `json:"sub"`
Iss string `json:"iss"` Iss string `json:"iss"`
Aud string `json:"aud"` Aud string `json:"aud"`
Name string `json:"preferred_username,omitempty"` Name string `json:"preferred_username,omitempty"`
DisplayName string `json:"name,omitempty"` DisplayName string `json:"name,omitempty"`
Email string `json:"email,omitempty"` Email string `json:"email,omitempty"`
Avatar string `json:"picture,omitempty"` EmailVerified bool `json:"email_verified,omitempty"`
Address string `json:"address,omitempty"` Avatar string `json:"picture,omitempty"`
Phone string `json:"phone,omitempty"` Address string `json:"address,omitempty"`
Groups []string `json:"groups,omitempty"` Phone string `json:"phone,omitempty"`
Groups []string `json:"groups,omitempty"`
} }
type ManagedAccount struct { type ManagedAccount struct {
@ -627,12 +628,10 @@ func AddUser(user *User) (bool, error) {
return false, nil return false, nil
} }
if user.PasswordType == "" && organization.PasswordType != "" { if user.PasswordType == "" || user.PasswordType == "plain" {
user.PasswordType = organization.PasswordType user.UpdateUserPassword(organization)
} }
user.UpdateUserPassword(organization)
err = user.UpdateUserHash() err = user.UpdateUserHash()
if err != nil { if err != nil {
return false, err return false, err
@ -759,6 +758,7 @@ func GetUserInfo(user *User, scope string, aud string, host string) *Userinfo {
} }
if strings.Contains(scope, "email") { if strings.Contains(scope, "email") {
resp.Email = user.Email resp.Email = user.Email
resp.EmailVerified = user.EmailVerified
} }
if strings.Contains(scope, "address") { if strings.Contains(scope, "address") {
resp.Address = user.Location resp.Address = user.Location

View File

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

View File

@ -14,11 +14,6 @@
package pp package pp
import (
"fmt"
"net/http"
)
type DummyPaymentProvider struct{} type DummyPaymentProvider struct{}
func NewDummyPaymentProvider() (*DummyPaymentProvider, error) { func NewDummyPaymentProvider() (*DummyPaymentProvider, error) {
@ -27,11 +22,10 @@ func NewDummyPaymentProvider() (*DummyPaymentProvider, error) {
} }
func (pp *DummyPaymentProvider) Pay(providerName string, productName string, payerName string, paymentName string, productDisplayName string, price float64, currency string, returnUrl string, notifyUrl string) (string, string, error) { func (pp *DummyPaymentProvider) Pay(providerName string, productName string, payerName string, paymentName string, productDisplayName string, price float64, currency string, returnUrl string, notifyUrl string) (string, string, error) {
payUrl := fmt.Sprintf("/payments/%s/result", paymentName) return returnUrl, "", nil
return payUrl, "", nil
} }
func (pp *DummyPaymentProvider) Notify(request *http.Request, body []byte, authorityPublicKey string, orderId string) (*NotifyResult, error) { func (pp *DummyPaymentProvider) Notify(body []byte, orderId string) (*NotifyResult, error) {
return &NotifyResult{ return &NotifyResult{
PaymentStatus: PaymentStatePaid, PaymentStatus: PaymentStatePaid,
}, nil }, nil

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -247,10 +247,10 @@ func initAPI() {
beego.Router("/api/send-sms", &controllers.ApiController{}, "POST:SendSms") beego.Router("/api/send-sms", &controllers.ApiController{}, "POST:SendSms")
beego.Router("/api/send-notification", &controllers.ApiController{}, "POST:SendNotification") beego.Router("/api/send-notification", &controllers.ApiController{}, "POST:SendNotification")
beego.Router("/api/webauthn/signup/begin", &controllers.ApiController{}, "Get:WebAuthnSignupBegin") beego.Router("/api/webauthn/signup/begin", &controllers.ApiController{}, "GET:WebAuthnSignupBegin")
beego.Router("/api/webauthn/signup/finish", &controllers.ApiController{}, "Post:WebAuthnSignupFinish") beego.Router("/api/webauthn/signup/finish", &controllers.ApiController{}, "POST:WebAuthnSignupFinish")
beego.Router("/api/webauthn/signin/begin", &controllers.ApiController{}, "Get:WebAuthnSigninBegin") beego.Router("/api/webauthn/signin/begin", &controllers.ApiController{}, "GET:WebAuthnSigninBegin")
beego.Router("/api/webauthn/signin/finish", &controllers.ApiController{}, "Post:WebAuthnSigninFinish") beego.Router("/api/webauthn/signin/finish", &controllers.ApiController{}, "POST:WebAuthnSigninFinish")
beego.Router("/api/mfa/setup/initiate", &controllers.ApiController{}, "POST:MfaSetupInitiate") beego.Router("/api/mfa/setup/initiate", &controllers.ApiController{}, "POST:MfaSetupInitiate")
beego.Router("/api/mfa/setup/verify", &controllers.ApiController{}, "POST:MfaSetupVerify") beego.Router("/api/mfa/setup/verify", &controllers.ApiController{}, "POST:MfaSetupVerify")
@ -273,7 +273,7 @@ func initAPI() {
beego.Router("/cas/:organization/:application/proxy", &controllers.RootController{}, "GET:CasProxy") beego.Router("/cas/:organization/:application/proxy", &controllers.RootController{}, "GET:CasProxy")
beego.Router("/cas/:organization/:application/validate", &controllers.RootController{}, "GET:CasValidate") beego.Router("/cas/:organization/:application/validate", &controllers.RootController{}, "GET:CasValidate")
beego.Router("/cas/:organization/:application/p3/serviceValidate", &controllers.RootController{}, "GET:CasP3ServiceAndProxyValidate") beego.Router("/cas/:organization/:application/p3/serviceValidate", &controllers.RootController{}, "GET:CasP3ServiceValidate")
beego.Router("/cas/:organization/:application/p3/proxyValidate", &controllers.RootController{}, "GET:CasP3ServiceAndProxyValidate") beego.Router("/cas/:organization/:application/p3/proxyValidate", &controllers.RootController{}, "GET:CasP3ProxyValidate")
beego.Router("/cas/:organization/:application/samlValidate", &controllers.RootController{}, "POST:SamlValidate") beego.Router("/cas/:organization/:application/samlValidate", &controllers.RootController{}, "POST:SamlValidate")
} }

View File

@ -16,6 +16,7 @@ package routers
import ( import (
"compress/gzip" "compress/gzip"
"fmt"
"io" "io"
"net/http" "net/http"
"os" "os"
@ -59,6 +60,13 @@ func StaticFilter(ctx *context.Context) {
path = "web/build/index.html" path = "web/build/index.html"
} }
if !util.FileExist(path) { if !util.FileExist(path) {
dir, err := os.Getwd()
if err != nil {
panic(err)
}
dir = strings.ReplaceAll(dir, "\\", "/")
errorText := fmt.Sprintf("The Casdoor frontend HTML file: \"index.html\" was not found, it should be placed at: \"%s/web/build/index.html\". For more information, see: https://casdoor.org/docs/basic/server-installation/#frontend-1", dir)
http.ServeContent(ctx.ResponseWriter, ctx.Request, "Casdoor frontend has encountered error...", time.Now(), strings.NewReader(errorText))
return return
} }

View File

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

View File

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

View File

@ -23,6 +23,7 @@ import (
"math/rand" "math/rand"
"os" "os"
"path/filepath" "path/filepath"
"regexp"
"strconv" "strconv"
"strings" "strings"
"time" "time"
@ -186,32 +187,6 @@ func IsStringsEmpty(strs ...string) bool {
return false return false
} }
func GetMaxLenStr(strs ...string) string {
m := 0
i := 0
for j, str := range strs {
l := len(str)
if l > m {
m = l
i = j
}
}
return strs[i]
}
func GetMinLenStr(strs ...string) string {
m := int(^uint(0) >> 1)
i := 0
for j, str := range strs {
l := len(str)
if l < m {
m = l
i = j
}
}
return strs[i]
}
func ReadStringFromPath(path string) string { func ReadStringFromPath(path string) string {
data, err := os.ReadFile(filepath.Clean(path)) data, err := os.ReadFile(filepath.Clean(path))
if err != nil { if err != nil {
@ -315,3 +290,13 @@ func ParseIdToString(input interface{}) (string, error) {
return "", fmt.Errorf("unsupported id type: %T", input) return "", fmt.Errorf("unsupported id type: %T", input)
} }
} }
func GetValueFromDataSourceName(key string, dataSourceName string) string {
reg := regexp.MustCompile(key + "=([^ ]+)")
matches := reg.FindStringSubmatch(dataSourceName)
if len(matches) >= 2 {
return matches[1]
}
return ""
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -101,6 +101,21 @@ class PaymentListPage extends BaseListPage {
); );
}, },
}, },
{
title: i18next.t("general:Organization"),
dataIndex: "owner",
key: "owner",
width: "120px",
sorter: true,
...this.getColumnSearchProps("owner"),
render: (text, record, index) => {
return (
<Link to={`/organizations/${text}`}>
{text}
</Link>
);
},
},
{ {
title: i18next.t("general:Provider"), title: i18next.t("general:Provider"),
dataIndex: "provider", dataIndex: "provider",
@ -117,21 +132,6 @@ class PaymentListPage extends BaseListPage {
); );
}, },
}, },
{
title: i18next.t("general:Organization"),
dataIndex: "owner",
key: "owner",
width: "120px",
sorter: true,
...this.getColumnSearchProps("owner"),
render: (text, record, index) => {
return (
<Link to={`/organizations/${text}`}>
{text}
</Link>
);
},
},
{ {
title: i18next.t("general:User"), title: i18next.t("general:User"),
dataIndex: "user", dataIndex: "user",
@ -264,7 +264,7 @@ class PaymentListPage extends BaseListPage {
value = params.type; value = params.type;
} }
this.setState({loading: true}); this.setState({loading: true});
PaymentBackend.getPayments(Setting.getRequestOrganization(this.props.account), Setting.isDefaultOrganizationSelected(this.props.account) ? "" : Setting.getRequestOrganization(this.props.account), params.pagination.current, params.pagination.pageSize, field, value, sortField, sortOrder) PaymentBackend.getPayments(Setting.isDefaultOrganizationSelected(this.props.account) ? "" : Setting.getRequestOrganization(this.props.account), params.pagination.current, params.pagination.pageSize, field, value, sortField, sortOrder)
.then((res) => { .then((res) => {
this.setState({ this.setState({
loading: false, loading: false,

View File

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

View File

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

View File

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

View File

@ -197,22 +197,27 @@ class PlanEditPage extends React.Component {
</Row> </Row>
<Row style={{marginTop: "20px"}} > <Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}> <Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("plan:Price per month"), i18next.t("plan:Price per month - Tooltip"))} : {Setting.getLabel(i18next.t("plan:Price"), i18next.t("plan:Price - Tooltip"))} :
</Col> </Col>
<Col span={22} > <Col span={22} >
<InputNumber value={this.state.plan.pricePerMonth} onChange={value => { <InputNumber value={this.state.plan.price} onChange={value => {
this.updatePlanField("pricePerMonth", value); this.updatePlanField("price", value);
}} /> }} />
</Col> </Col>
</Row> </Row>
<Row style={{marginTop: "20px"}} > <Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}> <Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("plan:Price per year"), i18next.t("plan:Price per year - Tooltip"))} : {Setting.getLabel(i18next.t("plan:Period"), i18next.t("plan:Period - Tooltip"))} :
</Col> </Col>
<Col span={22} > <Col span={22} >
<InputNumber value={this.state.plan.pricePerYear} onChange={value => { <Select virtual={false} style={{width: "100%"}} value={this.state.plan.period} onChange={value => {
this.updatePlanField("pricePerYear", value); this.updatePlanField("period", value);
}} /> }}
options={[
{value: "Monthly", label: "Monthly"},
{value: "Yearly", label: "Yearly"},
]}
/>
</Col> </Col>
</Row> </Row>
<Row style={{marginTop: "20px"}} > <Row style={{marginTop: "20px"}} >

View File

@ -32,9 +32,9 @@ class PlanListPage extends BaseListPage {
createdTime: moment().format(), createdTime: moment().format(),
displayName: `New Plan - ${randomName}`, displayName: `New Plan - ${randomName}`,
description: "", description: "",
pricePerMonth: 10, price: 10,
pricePerYear: 100,
currency: "USD", currency: "USD",
period: "Monthly",
isEnabled: true, isEnabled: true,
paymentProviders: [], paymentProviders: [],
role: "", role: "",
@ -136,18 +136,18 @@ class PlanListPage extends BaseListPage {
...this.getColumnSearchProps("currency"), ...this.getColumnSearchProps("currency"),
}, },
{ {
title: i18next.t("plan:Price per month"), title: i18next.t("plan:Price"),
dataIndex: "pricePerMonth", dataIndex: "price",
key: "pricePerMonth", key: "price",
width: "130px", width: "130px",
...this.getColumnSearchProps("pricePerMonth"), ...this.getColumnSearchProps("price"),
}, },
{ {
title: i18next.t("plan:Price per year"), title: i18next.t("plan:Period"),
dataIndex: "pricePerYear", dataIndex: "period",
key: "pricePerYear", key: "period",
width: "130px", width: "130px",
...this.getColumnSearchProps("pricePerYear"), ...this.getColumnSearchProps("period"),
}, },
{ {
title: i18next.t("general:Role"), title: i18next.t("general:Role"),

View File

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

View File

@ -98,6 +98,7 @@ class ProductEditPage extends React.Component {
} }
renderProduct() { renderProduct() {
const isCreatedByPlan = this.state.product.tag === "auto_created_product_for_plan";
return ( return (
<Card size="small" title={ <Card size="small" title={
<div> <div>
@ -107,12 +108,24 @@ class ProductEditPage extends React.Component {
{this.state.mode === "add" ? <Button style={{marginLeft: "20px"}} onClick={() => this.deleteProduct()}>{i18next.t("general:Cancel")}</Button> : null} {this.state.mode === "add" ? <Button style={{marginLeft: "20px"}} onClick={() => this.deleteProduct()}>{i18next.t("general:Cancel")}</Button> : null}
</div> </div>
} style={(Setting.isMobile()) ? {margin: "5px"} : {}} type="inner"> } style={(Setting.isMobile()) ? {margin: "5px"} : {}} type="inner">
<Row style={{marginTop: "10px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("general:Organization"), i18next.t("general:Organization - Tooltip"))} :
</Col>
<Col span={22} >
<Select virtual={false} style={{width: "100%"}} disabled={!Setting.isAdminUser(this.props.account) || isCreatedByPlan} value={this.state.product.owner} onChange={(value => {this.updateProductField("owner", value);})}>
{
this.state.organizations.map((organization, index) => <Option key={index} value={organization.name}>{organization.name}</Option>)
}
</Select>
</Col>
</Row>
<Row style={{marginTop: "20px"}} > <Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}> <Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("general:Name"), i18next.t("general:Name - Tooltip"))} : {Setting.getLabel(i18next.t("general:Name"), i18next.t("general:Name - Tooltip"))} :
</Col> </Col>
<Col span={22} > <Col span={22} >
<Input value={this.state.product.name} onChange={e => { <Input value={this.state.product.name} disabled={isCreatedByPlan} onChange={e => {
this.updateProductField("name", e.target.value); this.updateProductField("name", e.target.value);
}} /> }} />
</Col> </Col>
@ -127,18 +140,6 @@ class ProductEditPage extends React.Component {
}} /> }} />
</Col> </Col>
</Row> </Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("general:Organization"), i18next.t("general:Organization - Tooltip"))} :
</Col>
<Col span={22} >
<Select virtual={false} style={{width: "100%"}} disabled={!Setting.isAdminUser(this.props.account)} value={this.state.product.owner} onChange={(value => {this.updateProductField("owner", value);})}>
{
this.state.organizations.map((organization, index) => <Option key={index} value={organization.name}>{organization.name}</Option>)
}
</Select>
</Col>
</Row>
<Row style={{marginTop: "20px"}} > <Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}> <Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("product:Image"), i18next.t("product:Image - Tooltip"))} : {Setting.getLabel(i18next.t("product:Image"), i18next.t("product:Image - Tooltip"))} :
@ -171,7 +172,7 @@ class ProductEditPage extends React.Component {
{Setting.getLabel(i18next.t("user:Tag"), i18next.t("product:Tag - Tooltip"))} : {Setting.getLabel(i18next.t("user:Tag"), i18next.t("product:Tag - Tooltip"))} :
</Col> </Col>
<Col span={22} > <Col span={22} >
<Input value={this.state.product.tag} onChange={e => { <Input value={this.state.product.tag} disabled={isCreatedByPlan} onChange={e => {
this.updateProductField("tag", e.target.value); this.updateProductField("tag", e.target.value);
}} /> }} />
</Col> </Col>
@ -201,7 +202,7 @@ class ProductEditPage extends React.Component {
{Setting.getLabel(i18next.t("payment:Currency"), i18next.t("payment:Currency - Tooltip"))} : {Setting.getLabel(i18next.t("payment:Currency"), i18next.t("payment:Currency - Tooltip"))} :
</Col> </Col>
<Col span={22} > <Col span={22} >
<Select virtual={false} style={{width: "100%"}} value={this.state.product.currency} onChange={(value => { <Select virtual={false} style={{width: "100%"}} value={this.state.product.currency} disabled={isCreatedByPlan} onChange={(value => {
this.updateProductField("currency", value); this.updateProductField("currency", value);
})}> })}>
{ {
@ -218,7 +219,7 @@ class ProductEditPage extends React.Component {
{Setting.getLabel(i18next.t("product:Price"), i18next.t("product:Price - Tooltip"))} : {Setting.getLabel(i18next.t("product:Price"), i18next.t("product:Price - Tooltip"))} :
</Col> </Col>
<Col span={22} > <Col span={22} >
<InputNumber value={this.state.product.price} onChange={value => { <InputNumber value={this.state.product.price} disabled={isCreatedByPlan} onChange={value => {
this.updateProductField("price", value); this.updateProductField("price", value);
}} /> }} />
</Col> </Col>
@ -228,7 +229,7 @@ class ProductEditPage extends React.Component {
{Setting.getLabel(i18next.t("product:Quantity"), i18next.t("product:Quantity - Tooltip"))} : {Setting.getLabel(i18next.t("product:Quantity"), i18next.t("product:Quantity - Tooltip"))} :
</Col> </Col>
<Col span={22} > <Col span={22} >
<InputNumber value={this.state.product.quantity} onChange={value => { <InputNumber value={this.state.product.quantity} disabled={isCreatedByPlan} onChange={value => {
this.updateProductField("quantity", value); this.updateProductField("quantity", value);
}} /> }} />
</Col> </Col>
@ -238,7 +239,7 @@ class ProductEditPage extends React.Component {
{Setting.getLabel(i18next.t("product:Sold"), i18next.t("product:Sold - Tooltip"))} : {Setting.getLabel(i18next.t("product:Sold"), i18next.t("product:Sold - Tooltip"))} :
</Col> </Col>
<Col span={22} > <Col span={22} >
<InputNumber value={this.state.product.sold} onChange={value => { <InputNumber value={this.state.product.sold} disabled={isCreatedByPlan} onChange={value => {
this.updateProductField("sold", value); this.updateProductField("sold", value);
}} /> }} />
</Col> </Col>
@ -248,7 +249,7 @@ class ProductEditPage extends React.Component {
{Setting.getLabel(i18next.t("product:Payment providers"), i18next.t("product:Payment providers - Tooltip"))} : {Setting.getLabel(i18next.t("product:Payment providers"), i18next.t("product:Payment providers - Tooltip"))} :
</Col> </Col>
<Col span={22} > <Col span={22} >
<Select virtual={false} mode="multiple" style={{width: "100%"}} value={this.state.product.providers} onChange={(value => {this.updateProductField("providers", value);})}> <Select virtual={false} mode="multiple" style={{width: "100%"}} disabled={isCreatedByPlan} value={this.state.product.providers} onChange={(value => {this.updateProductField("providers", value);})}>
{ {
this.state.providers.map((provider, index) => <Option key={index} value={provider.name}>{provider.name}</Option>) this.state.providers.map((provider, index) => <Option key={index} value={provider.name}>{provider.name}</Option>)
} }
@ -312,7 +313,7 @@ class ProductEditPage extends React.Component {
submitProductEdit(willExist) { submitProductEdit(willExist) {
const product = Setting.deepCopy(this.state.product); const product = Setting.deepCopy(this.state.product);
ProductBackend.updateProduct(this.state.product.owner, this.state.productName, product) ProductBackend.updateProduct(this.state.organizationName, this.state.productName, product)
.then((res) => { .then((res) => {
if (res.status === "ok") { if (res.status === "ok") {
Setting.showMessage("success", i18next.t("general:Successfully saved")); Setting.showMessage("success", i18next.t("general:Successfully saved"));

View File

@ -253,11 +253,13 @@ class ProductListPage extends BaseListPage {
width: "230px", width: "230px",
fixed: (Setting.isMobile()) ? "false" : "right", fixed: (Setting.isMobile()) ? "false" : "right",
render: (text, record, index) => { render: (text, record, index) => {
const isCreatedByPlan = record.tag === "auto_created_product_for_plan";
return ( return (
<div> <div>
<Button style={{marginTop: "10px", marginBottom: "10px", marginRight: "10px"}} onClick={() => this.props.history.push(`/products/${record.owner}/${record.name}/buy`)}>{i18next.t("product:Buy")}</Button> <Button style={{marginTop: "10px", marginBottom: "10px", marginRight: "10px"}} onClick={() => this.props.history.push(`/products/${record.owner}/${record.name}/buy`)}>{i18next.t("product:Buy")}</Button>
<Button style={{marginTop: "10px", marginBottom: "10px", marginRight: "10px"}} type="primary" onClick={() => this.props.history.push(`/products/${record.owner}/${record.name}`)}>{i18next.t("general:Edit")}</Button> <Button style={{marginTop: "10px", marginBottom: "10px", marginRight: "10px"}} type="primary" onClick={() => this.props.history.push(`/products/${record.owner}/${record.name}`)}>{i18next.t("general:Edit")}</Button>
<PopconfirmModal <PopconfirmModal
disabled={isCreatedByPlan}
title={i18next.t("general:Sure to delete") + `: ${record.name} ?`} title={i18next.t("general:Sure to delete") + `: ${record.name} ?`}
onConfirm={() => this.deleteProduct(index)} onConfirm={() => this.deleteProduct(index)}
> >

View File

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

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

@ -0,0 +1,135 @@
// Copyright 2023 The Casdoor Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import React from "react";
import QRCode from "qrcode.react";
import {Button, Col, Row} from "antd";
import * as PaymentBackend from "./backend/PaymentBackend";
import * as Setting from "./Setting";
import * as ProviderBackend from "./backend/ProviderBackend";
import i18next from "i18next";
class QrCodePage extends React.Component {
constructor(props) {
super(props);
const params = new URLSearchParams(window.location.search);
this.state = {
classes: props,
owner: props.owner ?? (props.match?.params?.owner ?? null),
paymentName: props.paymentName ?? (props.match?.params?.paymentName ?? null),
providerName: props.providerName ?? params.get("providerName"),
payUrl: props.payUrl ?? params.get("payUrl"),
successUrl: props.successUrl ?? params.get("successUrl"),
provider: props.provider ?? null,
payment: null,
timer: null,
};
}
async getProvider() {
if (!this.state.owner || !this.state.providerName) {
return ;
}
try {
const res = await ProviderBackend.getProvider(this.state.owner, this.state.providerName);
if (res.status !== "ok") {
throw new Error(res.msg);
}
const provider = res.data;
this.setState({
provider: provider,
});
} catch (err) {
Setting.showMessage("error", err.message);
return ;
}
}
setNotifyTask() {
if (!this.state.owner || !this.state.paymentName) {
return ;
}
const notifyTask = async() => {
try {
const res = await PaymentBackend.notifyPayment(this.state.owner, this.state.paymentName);
if (res.status !== "ok") {
throw new Error(res.msg);
}
const payment = res.data;
if (payment.state !== "Created") {
Setting.goToLink(this.state.successUrl);
}
} catch (err) {
Setting.showMessage("error", err.message);
return ;
}
};
this.setState({
timer: setTimeout(async() => {
await notifyTask();
this.setNotifyTask();
}, 2000),
});
}
componentDidMount() {
if (this.props.onUpdateApplication) {
this.props.onUpdateApplication(null);
}
this.getProvider();
this.setNotifyTask();
}
componentWillUnmount() {
clearInterval(this.state.timer);
}
renderProviderInfo(provider) {
if (!provider) {
return null;
}
const text = i18next.t(`product:${provider.type}`);
return (
<Button style={{height: "50px", borderWidth: "2px"}} shape="round" icon={
<img style={{marginRight: "10px"}} width={36} height={36} src={Setting.getProviderLogoURL(provider)} alt={provider.displayName} />
} size={"large"} >
{
text
}
</Button>
);
}
render() {
if (!this.state.payUrl || !this.state.successUrl || !this.state.owner || !this.state.paymentName) {
return null;
}
return (
<div className="login-content">
<Col>
<Row style={{justifyContent: "center"}}>
{this.renderProviderInfo(this.state.provider)}
</Row>
<Row style={{marginTop: "10px", justifyContent: "center"}}>
<QRCode value={this.state.payUrl} size={this.props.size ?? 200} />
</Row>
</Col>
</div>
);
}
}
export default QrCodePage;

View File

@ -32,7 +32,8 @@ export const ServerUrl = "";
export const StaticBaseUrl = "https://cdn.casbin.org"; export const StaticBaseUrl = "https://cdn.casbin.org";
export const Countries = [{label: "English", key: "en", country: "US", alt: "English"}, export const Countries = [
{label: "English", key: "en", country: "US", alt: "English"},
{label: "Español", key: "es", country: "ES", alt: "Español"}, {label: "Español", key: "es", country: "ES", alt: "Español"},
{label: "Français", key: "fr", country: "FR", alt: "Français"}, {label: "Français", key: "fr", country: "FR", alt: "Français"},
{label: "Deutsch", key: "de", country: "DE", alt: "Deutsch"}, {label: "Deutsch", key: "de", country: "DE", alt: "Deutsch"},
@ -42,13 +43,19 @@ export const Countries = [{label: "English", key: "en", country: "US", alt: "Eng
{label: "한국어", key: "ko", country: "KR", alt: "한국어"}, {label: "한국어", key: "ko", country: "KR", alt: "한국어"},
{label: "Русский", key: "ru", country: "RU", alt: "Русский"}, {label: "Русский", key: "ru", country: "RU", alt: "Русский"},
{label: "TiếngViệt", key: "vi", country: "VN", alt: "TiếngViệt"}, {label: "TiếngViệt", key: "vi", country: "VN", alt: "TiếngViệt"},
{label: "Português", key: "pt", country: "BR", alt: "Português"}, {label: "Português", key: "pt", country: "PT", alt: "Português"},
{label: "Itariano", key: "it", country: "IT", alt: "Itariano"}, {label: "Italiano", key: "it", country: "IT", alt: "Italiano"},
{label: "Marley", key: "ms", country: "MY", alt: "Marley"}, {label: "Malay", key: "ms", country: "MY", alt: "Malay"},
{label: "Tkiš", key: "tr", country: "TR", alt: "Tkiš"}, {label: "Türkçe", key: "tr", country: "TR", alt: "Türkçe"},
{label: "لغة عربية", key: "ar", country: "DZ", alt: "لغة عربية"}, {label: "لغة عربية", key: "ar", country: "SA", alt: "لغة عربية"},
{label: "עִבְרִית", key: "he", country: "IL", alt: "עִבְרִית"}, {label: "עִבְרִית", key: "he", country: "IL", alt: "עִבְרִית"},
{label: "Filipino", key: "fi", country: "PH", alt: "Filipino"}, {label: "Nederlands", key: "nl", country: "NL", alt: "Nederlands"},
{label: "Polski", key: "pl", country: "PL", alt: "Polski"},
{label: "Suomi", key: "fi", country: "FI", alt: "Suomi"},
{label: "Svenska", key: "sv", country: "SE", alt: "Svenska"},
{label: "Українська", key: "uk", country: "UA", alt: "Українська"},
{label: "Қазақ", key: "kk", country: "KZ", alt: "Қазақ"},
{label: "فارسی", key: "fa", country: "IR", alt: "فارسی"},
]; ];
export function getThemeData(organization, application) { export function getThemeData(organization, application) {
@ -154,6 +161,10 @@ export const OtherProviderInfo = {
logo: `${StaticBaseUrl}/img/email_mailtrap.png`, logo: `${StaticBaseUrl}/img/email_mailtrap.png`,
url: "https://mailtrap.io", url: "https://mailtrap.io",
}, },
"Azure ACS": {
logo: `${StaticBaseUrl}/img/social_azure.png`,
url: "https://learn.microsoft.com/zh-cn/azure/communication-services",
},
}, },
Storage: { Storage: {
"Local File System": { "Local File System": {
@ -276,6 +287,70 @@ export const OtherProviderInfo = {
logo: `${StaticBaseUrl}/img/email_default.png`, logo: `${StaticBaseUrl}/img/email_default.png`,
url: "https://casdoor.org/docs/provider/notification/overview", url: "https://casdoor.org/docs/provider/notification/overview",
}, },
"DingTalk": {
logo: `${StaticBaseUrl}/img/social_dingtalk.png`,
url: "https://www.dingtalk.com/",
},
"Lark": {
logo: `${StaticBaseUrl}/img/social_lark.png`,
url: "https://www.larksuite.com/",
},
"Microsoft Teams": {
logo: `${StaticBaseUrl}/img/social_teams.png`,
url: "https://www.microsoft.com/microsoft-teams",
},
"Bark": {
logo: `${StaticBaseUrl}/img/social_bark.png`,
url: "https://apps.apple.com/us/app/bark-customed-notifications/id1403753865",
},
"Pushover": {
logo: `${StaticBaseUrl}/img/social_pushover.png`,
url: "https://pushover.net/",
},
"Pushbullet": {
logo: `${StaticBaseUrl}/img/social_pushbullet.png`,
url: "https://www.pushbullet.com/",
},
"Slack": {
logo: `${StaticBaseUrl}/img/social_slack.png`,
url: "https://slack.com/",
},
"Webpush": {
logo: `${StaticBaseUrl}/img/email_default.png`,
url: "https://developer.mozilla.org/en-US/docs/Web/API/Push_API",
},
"Discord": {
logo: `${StaticBaseUrl}/img/social_discord.png`,
url: "https://discord.com/",
},
"Google Chat": {
logo: `${StaticBaseUrl}/img/social_google_chat.png`,
url: "https://workspace.google.com/intl/en/products/chat/",
},
"Line": {
logo: `${StaticBaseUrl}/img/social_line.png`,
url: "https://line.me/",
},
"Matrix": {
logo: `${StaticBaseUrl}/img/social_matrix.png`,
url: "https://www.matrix.org/",
},
"Twitter": {
logo: `${StaticBaseUrl}/img/social_twitter.png`,
url: "https://twitter.com/",
},
"Reddit": {
logo: `${StaticBaseUrl}/img/social_reddit.png`,
url: "https://www.reddit.com/",
},
"Rocket Chat": {
logo: `${StaticBaseUrl}/img/social_rocket_chat.png`,
url: "https://rocket.chat/",
},
"Viber": {
logo: `${StaticBaseUrl}/img/social_viber.png`,
url: "https://www.viber.com/",
},
}, },
}; };
@ -901,6 +976,7 @@ export function getProviderTypeOptions(category) {
{id: "Default", name: "Default"}, {id: "Default", name: "Default"},
{id: "SUBMAIL", name: "SUBMAIL"}, {id: "SUBMAIL", name: "SUBMAIL"},
{id: "Mailtrap", name: "Mailtrap"}, {id: "Mailtrap", name: "Mailtrap"},
{id: "Azure ACS", name: "Azure ACS"},
] ]
); );
} else if (category === "SMS") { } else if (category === "SMS") {
@ -966,6 +1042,22 @@ export function getProviderTypeOptions(category) {
return ([ return ([
{id: "Telegram", name: "Telegram"}, {id: "Telegram", name: "Telegram"},
{id: "Custom HTTP", name: "Custom HTTP"}, {id: "Custom HTTP", name: "Custom HTTP"},
{id: "DingTalk", name: "DingTalk"},
{id: "Lark", name: "Lark"},
{id: "Microsoft Teams", name: "Microsoft Teams"},
{id: "Bark", name: "Bark"},
{id: "Pushover", name: "Pushover"},
{id: "Pushbullet", name: "Pushbullet"},
{id: "Slack", name: "Slack"},
{id: "Webpush", name: "Webpush"},
{id: "Discord", name: "Discord"},
{id: "Google Chat", name: "Google Chat"},
{id: "Line", name: "Line"},
{id: "Matrix", name: "Matrix"},
{id: "Twitter", name: "Twitter"},
{id: "Reddit", name: "Reddit"},
{id: "Rocket Chat", name: "Rocket Chat"},
{id: "Viber", name: "Viber"},
]); ]);
} else { } else {
return []; return [];

View File

@ -14,7 +14,7 @@
import moment from "moment"; import moment from "moment";
import React from "react"; import React from "react";
import {Button, Card, Col, DatePicker, Input, InputNumber, Row, Select} from "antd"; import {Button, Card, Col, DatePicker, Input, Row, Select} from "antd";
import * as OrganizationBackend from "./backend/OrganizationBackend"; import * as OrganizationBackend from "./backend/OrganizationBackend";
import * as PricingBackend from "./backend/PricingBackend"; import * as PricingBackend from "./backend/PricingBackend";
import * as PlanBackend from "./backend/PlanBackend"; import * as PlanBackend from "./backend/PlanBackend";
@ -171,16 +171,6 @@ class SubscriptionEditPage extends React.Component {
}} /> }} />
</Col> </Col>
</Row> </Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("subscription:Duration"), i18next.t("subscription:Duration - Tooltip"))}
</Col>
<Col span={22} >
<InputNumber value={this.state.subscription.duration} onChange={value => {
this.updateSubscriptionField("duration", value);
}} />
</Col>
</Row>
<Row style={{marginTop: "20px"}} > <Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}> <Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("subscription:Start time"), i18next.t("subscription:Start time - Tooltip"))} {Setting.getLabel(i18next.t("subscription:Start time"), i18next.t("subscription:Start time - Tooltip"))}
@ -201,6 +191,23 @@ class SubscriptionEditPage extends React.Component {
}} /> }} />
</Col> </Col>
</Row> </Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("plan:Period"), i18next.t("plan:Period - Tooltip"))} :
</Col>
<Col span={22} >
<Select
defaultValue={this.state.subscription.period === "" ? "Monthly" : this.state.subscription.period}
onChange={value => {
this.updateSubscriptionField("period", value);
}}
options={[
{value: "Monthly", label: "Monthly"},
{value: "Yearly", label: "Yearly"},
]}
/>
</Col>
</Row>
<Row style={{marginTop: "20px"}} > <Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}> <Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("general:User"), i18next.t("general:User - Tooltip"))} : {Setting.getLabel(i18next.t("general:User"), i18next.t("general:User - Tooltip"))} :

Some files were not shown because too many files have changed in this diff Show More