Self-hosted in-product purchase (#21804)
* Self-hosted admins can purchase licenses in-app when `ServiceSettings,SelfHostedPurchase` is true (the default) * Content Security Policy enables loading assets from `js.stripe.com/v3` when `ServiceSettings.SelfHostedPurchase` is true (the default). * Add `hosted_customer` API subpath * Add status of SelfHostedPurchase to telemetry config report. * Support showing admins self-hosted invoices when `ServiceSettings.SelfHostedPurchase` is true (the default) Co-authored-by: Conor Macpherson <116016004+ConorMacpherson@users.noreply.github.com>
Этот коммит содержится в:
коммит произвёл
GitHub
родитель
6fd174a95f
Коммит
a8fa3f29e9
@@ -4,18 +4,35 @@
|
|||||||
package api4
|
package api4
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/binary"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"reflect"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/mattermost/mattermost-server/v6/model"
|
"github.com/mattermost/mattermost-server/v6/model"
|
||||||
|
"github.com/mattermost/mattermost-server/v6/shared/mlog"
|
||||||
|
"github.com/mattermost/mattermost-server/v6/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
// APIs for self-hosted workspaces to communicate with the backing customer & payments system.
|
// APIs for self-hosted workspaces to communicate with the backing customer & payments system.
|
||||||
// Endpoints for cloud installations should not go in this file.
|
// Endpoints for cloud installations should not go in this file.
|
||||||
func (api *API) InitHostedCustomer() {
|
func (api *API) InitHostedCustomer() {
|
||||||
|
// POST /api/v4/hosted_customer/available
|
||||||
|
api.BaseRoutes.HostedCustomer.Handle("/signup_available", api.APISessionRequired(handleSignupAvailable)).Methods("GET")
|
||||||
// POST /api/v4/hosted_customer/bootstrap
|
// POST /api/v4/hosted_customer/bootstrap
|
||||||
api.BaseRoutes.HostedCustomer.Handle("/bootstrap", api.APISessionRequired(selfHostedBootstrap)).Methods("POST")
|
api.BaseRoutes.HostedCustomer.Handle("/bootstrap", api.APISessionRequired(selfHostedBootstrap)).Methods("POST")
|
||||||
|
// POST /api/v4/hosted_customer/customer
|
||||||
|
api.BaseRoutes.HostedCustomer.Handle("/customer", api.APISessionRequired(selfHostedCustomer)).Methods("POST")
|
||||||
|
// POST /api/v4/hosted_customer/confirm
|
||||||
|
api.BaseRoutes.HostedCustomer.Handle("/confirm", api.APISessionRequired(selfHostedConfirm)).Methods("POST")
|
||||||
|
// GET /api/v4/hosted_customer/invoices
|
||||||
|
api.BaseRoutes.HostedCustomer.Handle("/invoices", api.APISessionRequired(selfHostedInvoices)).Methods("GET")
|
||||||
|
// GET /api/v4/hosted_customer/invoices/{invoice_id:in_[A-Za-z0-9]+}/pdf
|
||||||
|
api.BaseRoutes.HostedCustomer.Handle("/invoices/{invoice_id:in_[A-Za-z0-9]+}/pdf", api.APISessionRequired(selfHostedInvoicePDF)).Methods("GET")
|
||||||
}
|
}
|
||||||
|
|
||||||
func ensureSelfHostedAdmin(c *Context, where string) {
|
func ensureSelfHostedAdmin(c *Context, where string) {
|
||||||
@@ -32,21 +49,22 @@ func ensureSelfHostedAdmin(c *Context, where string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func checkSelfHostedFirstTimePurchaseEnabled(c *Context) bool {
|
func checkSelfHostedPurchaseEnabled(c *Context) bool {
|
||||||
config := c.App.Config()
|
config := c.App.Config()
|
||||||
if config == nil {
|
if config == nil {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
enabled := config.ServiceSettings.SelfHostedFirstTimePurchase
|
enabled := config.ServiceSettings.SelfHostedPurchase
|
||||||
return enabled != nil && *enabled
|
return enabled != nil && *enabled
|
||||||
}
|
}
|
||||||
|
|
||||||
func selfHostedBootstrap(c *Context, w http.ResponseWriter, r *http.Request) {
|
func selfHostedBootstrap(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||||
where := "Api4.selfHostedBootstrap"
|
const where = "Api4.selfHostedBootstrap"
|
||||||
if !checkSelfHostedFirstTimePurchaseEnabled(c) {
|
if !checkSelfHostedPurchaseEnabled(c) {
|
||||||
c.Err = model.NewAppError(where, "api.cloud.app_error", nil, "", http.StatusNotImplemented)
|
c.Err = model.NewAppError(where, "api.cloud.app_error", nil, "", http.StatusNotImplemented)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
reset := r.URL.Query().Get("reset") == "true"
|
||||||
ensureSelfHostedAdmin(c, where)
|
ensureSelfHostedAdmin(c, where)
|
||||||
if c.Err != nil {
|
if c.Err != nil {
|
||||||
return
|
return
|
||||||
@@ -58,7 +76,7 @@ func selfHostedBootstrap(c *Context, w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
signupProgress, err := c.App.Cloud().BootstrapSelfHostedSignup(model.BootstrapSelfHostedSignupRequest{Email: user.Email})
|
signupProgress, err := c.App.Cloud().BootstrapSelfHostedSignup(model.BootstrapSelfHostedSignupRequest{Email: user.Email, Reset: reset})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.Err = model.NewAppError(where, "api.cloud.app_error", nil, "", http.StatusInternalServerError)
|
c.Err = model.NewAppError(where, "api.cloud.app_error", nil, "", http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
@@ -71,3 +89,186 @@ func selfHostedBootstrap(c *Context, w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
w.Write(json)
|
w.Write(json)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func selfHostedCustomer(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||||
|
const where = "Api4.selfHostedCustomer"
|
||||||
|
ensureSelfHostedAdmin(c, where)
|
||||||
|
if c.Err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !checkSelfHostedPurchaseEnabled(c) {
|
||||||
|
c.Err = model.NewAppError(where, "api.cloud.app_error", nil, "", http.StatusNotImplemented)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
bodyBytes, err := io.ReadAll(r.Body)
|
||||||
|
if err != nil {
|
||||||
|
c.Err = model.NewAppError(where, "api.cloud.app_error", nil, "", http.StatusBadRequest).Wrap(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var form *model.SelfHostedCustomerForm
|
||||||
|
if err = json.Unmarshal(bodyBytes, &form); err != nil {
|
||||||
|
c.Err = model.NewAppError(where, "api.cloud.app_error", nil, "", http.StatusBadRequest).Wrap(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
user, userErr := c.App.GetUser(c.AppContext.Session().UserId)
|
||||||
|
if userErr != nil {
|
||||||
|
c.Err = userErr
|
||||||
|
return
|
||||||
|
}
|
||||||
|
customerResponse, err := c.App.Cloud().CreateCustomerSelfHostedSignup(*form, user.Email)
|
||||||
|
if err != nil {
|
||||||
|
c.Err = model.NewAppError(where, "api.cloud.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
json, err := json.Marshal(customerResponse)
|
||||||
|
if err != nil {
|
||||||
|
c.Err = model.NewAppError(where, "api.cloud.request_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Write(json)
|
||||||
|
}
|
||||||
|
|
||||||
|
func selfHostedConfirm(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||||
|
const where = "Api4.selfHostedConfirm"
|
||||||
|
ensureSelfHostedAdmin(c, where)
|
||||||
|
if c.Err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !checkSelfHostedPurchaseEnabled(c) {
|
||||||
|
c.Err = model.NewAppError(where, "api.cloud.app_error", nil, "", http.StatusNotImplemented)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
bodyBytes, err := io.ReadAll(r.Body)
|
||||||
|
if err != nil {
|
||||||
|
c.Err = model.NewAppError(where, "api.cloud.app_error", nil, "", http.StatusBadRequest).Wrap(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var confirm model.SelfHostedConfirmPaymentMethodRequest
|
||||||
|
err = json.Unmarshal(bodyBytes, &confirm)
|
||||||
|
if err != nil {
|
||||||
|
c.Err = model.NewAppError(where, "api.cloud.request_error", nil, "", http.StatusBadRequest).Wrap(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
user, userErr := c.App.GetUser(c.AppContext.Session().UserId)
|
||||||
|
if userErr != nil {
|
||||||
|
c.Err = userErr
|
||||||
|
return
|
||||||
|
}
|
||||||
|
confirmResponse, err := c.App.Cloud().ConfirmSelfHostedSignup(confirm, user.Email)
|
||||||
|
if err != nil {
|
||||||
|
if confirmResponse != nil {
|
||||||
|
c.App.NotifySelfHostedSignupProgress(confirmResponse.Progress, user.Id)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err.Error() == fmt.Sprintf("%d", http.StatusUnprocessableEntity) {
|
||||||
|
c.Err = model.NewAppError(where, "api.cloud.app_error", nil, "", http.StatusUnprocessableEntity).Wrap(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.Err = model.NewAppError(where, "api.cloud.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
license, err := c.App.Srv().Platform().SaveLicense([]byte(confirmResponse.License))
|
||||||
|
// dealing with an AppError
|
||||||
|
if !(reflect.ValueOf(err).Kind() == reflect.Ptr && reflect.ValueOf(err).IsNil()) {
|
||||||
|
if confirmResponse != nil {
|
||||||
|
c.App.NotifySelfHostedSignupProgress(confirmResponse.Progress, user.Id)
|
||||||
|
}
|
||||||
|
c.Err = model.NewAppError(where, "api.cloud.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
clientResponse, err := json.Marshal(model.SelfHostedSignupConfirmClientResponse{
|
||||||
|
License: utils.GetClientLicense(license),
|
||||||
|
Progress: confirmResponse.Progress,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
if confirmResponse != nil {
|
||||||
|
c.App.NotifySelfHostedSignupProgress(confirmResponse.Progress, user.Id)
|
||||||
|
}
|
||||||
|
c.Err = model.NewAppError(where, "api.cloud.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
err := c.App.Cloud().ConfirmSelfHostedSignupLicenseApplication()
|
||||||
|
if err != nil {
|
||||||
|
c.Logger.Warn("Unable to confirm license application", mlog.Err(err))
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
_, _ = w.Write(clientResponse)
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleSignupAvailable(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||||
|
const where = "Api4.handleSignupAvailable"
|
||||||
|
ensureSelfHostedAdmin(c, where)
|
||||||
|
if c.Err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !checkSelfHostedPurchaseEnabled(c) {
|
||||||
|
c.Err = model.NewAppError(where, "api.cloud.app_error", nil, "", http.StatusNotImplemented)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := c.App.Cloud().SelfHostedSignupAvailable(); err != nil {
|
||||||
|
c.Err = model.NewAppError(where, "api.server.hosted_signup_unavailable.error", nil, "", http.StatusNotImplemented)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ReturnStatusOK(w)
|
||||||
|
}
|
||||||
|
|
||||||
|
func selfHostedInvoices(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||||
|
const where = "Api4.selfHostedInvoices"
|
||||||
|
ensureSelfHostedAdmin(c, where)
|
||||||
|
if c.Err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
invoices, err := c.App.Cloud().GetSelfHostedInvoices()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
c.Err = model.NewAppError(where, "api.cloud.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
json, err := json.Marshal(invoices)
|
||||||
|
if err != nil {
|
||||||
|
c.Err = model.NewAppError(where, "api.cloud.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Write(json)
|
||||||
|
}
|
||||||
|
|
||||||
|
func selfHostedInvoicePDF(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||||
|
const where = "Api4.selfHostedInvoicePDF"
|
||||||
|
ensureSelfHostedAdmin(c, where)
|
||||||
|
if c.Err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
pdfData, filename, appErr := c.App.Cloud().GetSelfHostedInvoicePDF(c.Params.InvoiceId)
|
||||||
|
if appErr != nil {
|
||||||
|
c.Err = model.NewAppError("Api4.getSubscriptionInvoicePDF", "api.cloud.request_error", nil, appErr.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
writeFileResponse(
|
||||||
|
filename,
|
||||||
|
"application/pdf",
|
||||||
|
int64(binary.Size(pdfData)),
|
||||||
|
time.Now(),
|
||||||
|
*c.App.Config().ServiceSettings.WebserverMode,
|
||||||
|
bytes.NewReader(pdfData),
|
||||||
|
false,
|
||||||
|
w,
|
||||||
|
r,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ func TestSelfHostedBootstrap(t *testing.T) {
|
|||||||
|
|
||||||
os.Setenv("MM_SERVICESETTINGS_SELFHOSTEDFIRSTTIMEPURCHASE", "false")
|
os.Setenv("MM_SERVICESETTINGS_SELFHOSTEDFIRSTTIMEPURCHASE", "false")
|
||||||
defer os.Unsetenv("MM_SERVICESETTINGS_SELFHOSTEDFIRSTTIMEPURCHASE")
|
defer os.Unsetenv("MM_SERVICESETTINGS_SELFHOSTEDFIRSTTIMEPURCHASE")
|
||||||
th.App.UpdateConfig(func(cfg *model.Config) { cfg.ServiceSettings.SelfHostedFirstTimePurchase = &valFalse })
|
th.App.UpdateConfig(func(cfg *model.Config) { cfg.ServiceSettings.SelfHostedPurchase = &valFalse })
|
||||||
th.App.ReloadConfig()
|
th.App.ReloadConfig()
|
||||||
|
|
||||||
_, r, err := th.Client.BootstrapSelfHostedSignup(model.BootstrapSelfHostedSignupRequest{Email: th.SystemAdminUser.Email})
|
_, r, err := th.Client.BootstrapSelfHostedSignup(model.BootstrapSelfHostedSignupRequest{Email: th.SystemAdminUser.Email})
|
||||||
@@ -45,7 +45,7 @@ func TestSelfHostedBootstrap(t *testing.T) {
|
|||||||
th.App.Srv().SetLicense(model.NewTestLicense("cloud"))
|
th.App.Srv().SetLicense(model.NewTestLicense("cloud"))
|
||||||
os.Setenv("MM_SERVICESETTINGS_SELFHOSTEDFIRSTTIMEPURCHASE", "true")
|
os.Setenv("MM_SERVICESETTINGS_SELFHOSTEDFIRSTTIMEPURCHASE", "true")
|
||||||
defer os.Unsetenv("MM_SERVICESETTINGS_SELFHOSTEDFIRSTTIMEPURCHASE")
|
defer os.Unsetenv("MM_SERVICESETTINGS_SELFHOSTEDFIRSTTIMEPURCHASE")
|
||||||
th.App.UpdateConfig(func(cfg *model.Config) { cfg.ServiceSettings.SelfHostedFirstTimePurchase = &valTrue })
|
th.App.UpdateConfig(func(cfg *model.Config) { cfg.ServiceSettings.SelfHostedPurchase = &valTrue })
|
||||||
th.App.ReloadConfig()
|
th.App.ReloadConfig()
|
||||||
|
|
||||||
_, r, err := th.Client.BootstrapSelfHostedSignup(model.BootstrapSelfHostedSignupRequest{Email: th.SystemAdminUser.Email})
|
_, r, err := th.Client.BootstrapSelfHostedSignup(model.BootstrapSelfHostedSignupRequest{Email: th.SystemAdminUser.Email})
|
||||||
@@ -62,7 +62,7 @@ func TestSelfHostedBootstrap(t *testing.T) {
|
|||||||
|
|
||||||
os.Setenv("MM_SERVICESETTINGS_SELFHOSTEDFIRSTTIMEPURCHASE", "true")
|
os.Setenv("MM_SERVICESETTINGS_SELFHOSTEDFIRSTTIMEPURCHASE", "true")
|
||||||
defer os.Unsetenv("MM_SERVICESETTINGS_SELFHOSTEDFIRSTTIMEPURCHASE")
|
defer os.Unsetenv("MM_SERVICESETTINGS_SELFHOSTEDFIRSTTIMEPURCHASE")
|
||||||
th.App.UpdateConfig(func(cfg *model.Config) { cfg.ServiceSettings.SelfHostedFirstTimePurchase = &valTrue })
|
th.App.UpdateConfig(func(cfg *model.Config) { cfg.ServiceSettings.SelfHostedPurchase = &valTrue })
|
||||||
th.App.ReloadConfig()
|
th.App.ReloadConfig()
|
||||||
|
|
||||||
_, r, err := th.Client.BootstrapSelfHostedSignup(model.BootstrapSelfHostedSignupRequest{Email: th.SystemAdminUser.Email})
|
_, r, err := th.Client.BootstrapSelfHostedSignup(model.BootstrapSelfHostedSignupRequest{Email: th.SystemAdminUser.Email})
|
||||||
@@ -79,7 +79,7 @@ func TestSelfHostedBootstrap(t *testing.T) {
|
|||||||
|
|
||||||
os.Setenv("MM_SERVICESETTINGS_SELFHOSTEDFIRSTTIMEPURCHASE", "true")
|
os.Setenv("MM_SERVICESETTINGS_SELFHOSTEDFIRSTTIMEPURCHASE", "true")
|
||||||
defer os.Unsetenv("MM_SERVICESETTINGS_SELFHOSTEDFIRSTTIMEPURCHASE")
|
defer os.Unsetenv("MM_SERVICESETTINGS_SELFHOSTEDFIRSTTIMEPURCHASE")
|
||||||
th.App.UpdateConfig(func(cfg *model.Config) { cfg.ServiceSettings.SelfHostedFirstTimePurchase = &valTrue })
|
th.App.UpdateConfig(func(cfg *model.Config) { cfg.ServiceSettings.SelfHostedPurchase = &valTrue })
|
||||||
th.App.ReloadConfig()
|
th.App.ReloadConfig()
|
||||||
cloud := mocks.CloudInterface{}
|
cloud := mocks.CloudInterface{}
|
||||||
|
|
||||||
|
|||||||
@@ -916,6 +916,7 @@ type AppIface interface {
|
|||||||
Notification() einterfaces.NotificationInterface
|
Notification() einterfaces.NotificationInterface
|
||||||
NotificationsLog() *mlog.Logger
|
NotificationsLog() *mlog.Logger
|
||||||
NotifyAndSetWarnMetricAck(warnMetricId string, sender *model.User, forceAck bool, isBot bool) *model.AppError
|
NotifyAndSetWarnMetricAck(warnMetricId string, sender *model.User, forceAck bool, isBot bool) *model.AppError
|
||||||
|
NotifySelfHostedSignupProgress(progress string, userId string)
|
||||||
NotifySharedChannelUserUpdate(user *model.User)
|
NotifySharedChannelUserUpdate(user *model.User)
|
||||||
OpenInteractiveDialog(request model.OpenDialogRequest) *model.AppError
|
OpenInteractiveDialog(request model.OpenDialogRequest) *model.AppError
|
||||||
OriginChecker() func(*http.Request) bool
|
OriginChecker() func(*http.Request) bool
|
||||||
|
|||||||
21
app/hosted_customer.go
Обычный файл
21
app/hosted_customer.go
Обычный файл
@@ -0,0 +1,21 @@
|
|||||||
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
package app
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/mattermost/mattermost-server/v6/model"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (a *App) NotifySelfHostedSignupProgress(progress string, userId string) {
|
||||||
|
// this is an event only the relevant admin should receive.
|
||||||
|
// If there is no progress, there is nothing to report.
|
||||||
|
// If there is no userId, we do not want to mistakenly broadcast to all users.
|
||||||
|
if progress == "" || userId == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
message := model.NewWebSocketEvent(model.WebsocketEventHostedCustomerSignupProgressUpdated, "", "", userId, nil, "")
|
||||||
|
message.Add("progress", progress)
|
||||||
|
|
||||||
|
a.Srv().Platform().Publish(message)
|
||||||
|
}
|
||||||
@@ -12632,6 +12632,21 @@ func (a *OpenTracingAppLayer) NotifyAndSetWarnMetricAck(warnMetricId string, sen
|
|||||||
return resultVar0
|
return resultVar0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (a *OpenTracingAppLayer) NotifySelfHostedSignupProgress(progress string, userId string) {
|
||||||
|
origCtx := a.ctx
|
||||||
|
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.NotifySelfHostedSignupProgress")
|
||||||
|
|
||||||
|
a.ctx = newCtx
|
||||||
|
a.app.Srv().Store().SetContext(newCtx)
|
||||||
|
defer func() {
|
||||||
|
a.app.Srv().Store().SetContext(origCtx)
|
||||||
|
a.ctx = origCtx
|
||||||
|
}()
|
||||||
|
|
||||||
|
defer span.Finish()
|
||||||
|
a.app.NotifySelfHostedSignupProgress(progress, userId)
|
||||||
|
}
|
||||||
|
|
||||||
func (a *OpenTracingAppLayer) NotifySessionsExpired() error {
|
func (a *OpenTracingAppLayer) NotifySessionsExpired() error {
|
||||||
origCtx := a.ctx
|
origCtx := a.ctx
|
||||||
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.NotifySessionsExpired")
|
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.NotifySessionsExpired")
|
||||||
|
|||||||
@@ -33,7 +33,15 @@ type CloudInterface interface {
|
|||||||
GetLicenseRenewalStatus(userID, token string) error
|
GetLicenseRenewalStatus(userID, token string) error
|
||||||
InvalidateCaches() error
|
InvalidateCaches() error
|
||||||
|
|
||||||
|
// hosted customer methods
|
||||||
|
SelfHostedSignupAvailable() error
|
||||||
BootstrapSelfHostedSignup(req model.BootstrapSelfHostedSignupRequest) (*model.BootstrapSelfHostedSignupResponse, error)
|
BootstrapSelfHostedSignup(req model.BootstrapSelfHostedSignupRequest) (*model.BootstrapSelfHostedSignupResponse, error)
|
||||||
|
CreateCustomerSelfHostedSignup(req model.SelfHostedCustomerForm, requesterEmail string) (*model.SelfHostedSignupCustomerResponse, error)
|
||||||
|
ConfirmSelfHostedSignup(req model.SelfHostedConfirmPaymentMethodRequest, requesterEmail string) (*model.SelfHostedSignupConfirmResponse, error)
|
||||||
|
ConfirmSelfHostedSignupLicenseApplication() error
|
||||||
|
GetSelfHostedInvoices() ([]*model.Invoice, error)
|
||||||
|
GetSelfHostedInvoicePDF(invoiceID string) ([]byte, string, error)
|
||||||
|
|
||||||
CreateOrUpdateSubscriptionHistoryEvent(userID string, userCount int) (*model.SubscriptionHistory, error)
|
CreateOrUpdateSubscriptionHistoryEvent(userID string, userCount int) (*model.SubscriptionHistory, error)
|
||||||
HandleLicenseChange() error
|
HandleLicenseChange() error
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -74,6 +74,43 @@ func (_m *CloudInterface) ConfirmCustomerPayment(userID string, confirmRequest *
|
|||||||
return r0
|
return r0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ConfirmSelfHostedSignup provides a mock function with given fields: req, requesterEmail
|
||||||
|
func (_m *CloudInterface) ConfirmSelfHostedSignup(req model.SelfHostedConfirmPaymentMethodRequest, requesterEmail string) (*model.SelfHostedSignupConfirmResponse, error) {
|
||||||
|
ret := _m.Called(req, requesterEmail)
|
||||||
|
|
||||||
|
var r0 *model.SelfHostedSignupConfirmResponse
|
||||||
|
if rf, ok := ret.Get(0).(func(model.SelfHostedConfirmPaymentMethodRequest, string) *model.SelfHostedSignupConfirmResponse); ok {
|
||||||
|
r0 = rf(req, requesterEmail)
|
||||||
|
} else {
|
||||||
|
if ret.Get(0) != nil {
|
||||||
|
r0 = ret.Get(0).(*model.SelfHostedSignupConfirmResponse)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var r1 error
|
||||||
|
if rf, ok := ret.Get(1).(func(model.SelfHostedConfirmPaymentMethodRequest, string) error); ok {
|
||||||
|
r1 = rf(req, requesterEmail)
|
||||||
|
} else {
|
||||||
|
r1 = ret.Error(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
return r0, r1
|
||||||
|
}
|
||||||
|
|
||||||
|
// ConfirmSelfHostedSignupLicenseApplication provides a mock function with given fields:
|
||||||
|
func (_m *CloudInterface) ConfirmSelfHostedSignupLicenseApplication() error {
|
||||||
|
ret := _m.Called()
|
||||||
|
|
||||||
|
var r0 error
|
||||||
|
if rf, ok := ret.Get(0).(func() error); ok {
|
||||||
|
r0 = rf()
|
||||||
|
} else {
|
||||||
|
r0 = ret.Error(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
return r0
|
||||||
|
}
|
||||||
|
|
||||||
// CreateCustomerPayment provides a mock function with given fields: userID
|
// CreateCustomerPayment provides a mock function with given fields: userID
|
||||||
func (_m *CloudInterface) CreateCustomerPayment(userID string) (*model.StripeSetupIntent, error) {
|
func (_m *CloudInterface) CreateCustomerPayment(userID string) (*model.StripeSetupIntent, error) {
|
||||||
ret := _m.Called(userID)
|
ret := _m.Called(userID)
|
||||||
@@ -97,6 +134,29 @@ func (_m *CloudInterface) CreateCustomerPayment(userID string) (*model.StripeSet
|
|||||||
return r0, r1
|
return r0, r1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CreateCustomerSelfHostedSignup provides a mock function with given fields: req, requesterEmail
|
||||||
|
func (_m *CloudInterface) CreateCustomerSelfHostedSignup(req model.SelfHostedCustomerForm, requesterEmail string) (*model.SelfHostedSignupCustomerResponse, error) {
|
||||||
|
ret := _m.Called(req, requesterEmail)
|
||||||
|
|
||||||
|
var r0 *model.SelfHostedSignupCustomerResponse
|
||||||
|
if rf, ok := ret.Get(0).(func(model.SelfHostedCustomerForm, string) *model.SelfHostedSignupCustomerResponse); ok {
|
||||||
|
r0 = rf(req, requesterEmail)
|
||||||
|
} else {
|
||||||
|
if ret.Get(0) != nil {
|
||||||
|
r0 = ret.Get(0).(*model.SelfHostedSignupCustomerResponse)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var r1 error
|
||||||
|
if rf, ok := ret.Get(1).(func(model.SelfHostedCustomerForm, string) error); ok {
|
||||||
|
r1 = rf(req, requesterEmail)
|
||||||
|
} else {
|
||||||
|
r1 = ret.Error(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
return r0, r1
|
||||||
|
}
|
||||||
|
|
||||||
// CreateOrUpdateSubscriptionHistoryEvent provides a mock function with given fields: userID, userCount
|
// CreateOrUpdateSubscriptionHistoryEvent provides a mock function with given fields: userID, userCount
|
||||||
func (_m *CloudInterface) CreateOrUpdateSubscriptionHistoryEvent(userID string, userCount int) (*model.SubscriptionHistory, error) {
|
func (_m *CloudInterface) CreateOrUpdateSubscriptionHistoryEvent(userID string, userCount int) (*model.SubscriptionHistory, error) {
|
||||||
ret := _m.Called(userID, userCount)
|
ret := _m.Called(userID, userCount)
|
||||||
@@ -279,6 +339,59 @@ func (_m *CloudInterface) GetLicenseRenewalStatus(userID string, token string) e
|
|||||||
return r0
|
return r0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetSelfHostedInvoicePDF provides a mock function with given fields: invoiceID
|
||||||
|
func (_m *CloudInterface) GetSelfHostedInvoicePDF(invoiceID string) ([]byte, string, error) {
|
||||||
|
ret := _m.Called(invoiceID)
|
||||||
|
|
||||||
|
var r0 []byte
|
||||||
|
if rf, ok := ret.Get(0).(func(string) []byte); ok {
|
||||||
|
r0 = rf(invoiceID)
|
||||||
|
} else {
|
||||||
|
if ret.Get(0) != nil {
|
||||||
|
r0 = ret.Get(0).([]byte)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var r1 string
|
||||||
|
if rf, ok := ret.Get(1).(func(string) string); ok {
|
||||||
|
r1 = rf(invoiceID)
|
||||||
|
} else {
|
||||||
|
r1 = ret.Get(1).(string)
|
||||||
|
}
|
||||||
|
|
||||||
|
var r2 error
|
||||||
|
if rf, ok := ret.Get(2).(func(string) error); ok {
|
||||||
|
r2 = rf(invoiceID)
|
||||||
|
} else {
|
||||||
|
r2 = ret.Error(2)
|
||||||
|
}
|
||||||
|
|
||||||
|
return r0, r1, r2
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetSelfHostedInvoices provides a mock function with given fields:
|
||||||
|
func (_m *CloudInterface) GetSelfHostedInvoices() ([]*model.Invoice, error) {
|
||||||
|
ret := _m.Called()
|
||||||
|
|
||||||
|
var r0 []*model.Invoice
|
||||||
|
if rf, ok := ret.Get(0).(func() []*model.Invoice); ok {
|
||||||
|
r0 = rf()
|
||||||
|
} else {
|
||||||
|
if ret.Get(0) != nil {
|
||||||
|
r0 = ret.Get(0).([]*model.Invoice)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var r1 error
|
||||||
|
if rf, ok := ret.Get(1).(func() error); ok {
|
||||||
|
r1 = rf()
|
||||||
|
} else {
|
||||||
|
r1 = ret.Error(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
return r0, r1
|
||||||
|
}
|
||||||
|
|
||||||
// GetSelfHostedProducts provides a mock function with given fields: userID
|
// GetSelfHostedProducts provides a mock function with given fields: userID
|
||||||
func (_m *CloudInterface) GetSelfHostedProducts(userID string) ([]*model.Product, error) {
|
func (_m *CloudInterface) GetSelfHostedProducts(userID string) ([]*model.Product, error) {
|
||||||
ret := _m.Called(userID)
|
ret := _m.Called(userID)
|
||||||
@@ -376,6 +489,20 @@ func (_m *CloudInterface) RequestCloudTrial(userID string, subscriptionID string
|
|||||||
return r0, r1
|
return r0, r1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SelfHostedSignupAvailable provides a mock function with given fields:
|
||||||
|
func (_m *CloudInterface) SelfHostedSignupAvailable() error {
|
||||||
|
ret := _m.Called()
|
||||||
|
|
||||||
|
var r0 error
|
||||||
|
if rf, ok := ret.Get(0).(func() error); ok {
|
||||||
|
r0 = rf()
|
||||||
|
} else {
|
||||||
|
r0 = ret.Error(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
return r0
|
||||||
|
}
|
||||||
|
|
||||||
// UpdateCloudCustomer provides a mock function with given fields: userID, customerInfo
|
// UpdateCloudCustomer provides a mock function with given fields: userID, customerInfo
|
||||||
func (_m *CloudInterface) UpdateCloudCustomer(userID string, customerInfo *model.CloudCustomerInfo) (*model.CloudCustomer, error) {
|
func (_m *CloudInterface) UpdateCloudCustomer(userID string, customerInfo *model.CloudCustomerInfo) (*model.CloudCustomer, error) {
|
||||||
ret := _m.Called(userID, customerInfo)
|
ret := _m.Called(userID, customerInfo)
|
||||||
|
|||||||
@@ -2543,6 +2543,10 @@
|
|||||||
"id": "api.scheme.patch_scheme.license.error",
|
"id": "api.scheme.patch_scheme.license.error",
|
||||||
"translation": "Your license does not support update permissions schemes"
|
"translation": "Your license does not support update permissions schemes"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"id": "api.server.hosted_signup_unavailable.error",
|
||||||
|
"translation": "Portal unavailable for self-hosted signup."
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"id": "api.server.license_up_for_renewal.error_generating_link",
|
"id": "api.server.license_up_for_renewal.error_generating_link",
|
||||||
"translation": "Failed to generate the license renewal link"
|
"translation": "Failed to generate the license renewal link"
|
||||||
|
|||||||
@@ -8558,6 +8558,72 @@ func (c *Client4) GetNewTeamMembersSince(teamID string, timeRange string, page i
|
|||||||
return newTeamMembersList, BuildResponse(r), nil
|
return newTeamMembersList, BuildResponse(r), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *Client4) SelfHostedSignupAvailable() (*Response, error) {
|
||||||
|
r, err := c.DoAPIGet(c.hostedCustomerRoute()+"/signup_available", "")
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return BuildResponse(r), err
|
||||||
|
}
|
||||||
|
defer closeBody(r)
|
||||||
|
|
||||||
|
return BuildResponse(r), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client4) SelfHostedSignupCustomer(form *SelfHostedCustomerForm) (*Response, *SelfHostedSignupCustomerResponse, error) {
|
||||||
|
payloadBytes, err := json.Marshal(form)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, NewAppError("SelfHostedSignupCustomer", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
r, err := c.DoAPIPost(c.hostedCustomerRoute()+"/customer", string(payloadBytes))
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return BuildResponse(r), nil, err
|
||||||
|
}
|
||||||
|
data, err := io.ReadAll(r.Body)
|
||||||
|
if err != nil {
|
||||||
|
return BuildResponse(r), nil, err
|
||||||
|
}
|
||||||
|
defer closeBody(r)
|
||||||
|
|
||||||
|
response := SelfHostedSignupCustomerResponse{}
|
||||||
|
err = json.Unmarshal(data, &response)
|
||||||
|
if err != nil {
|
||||||
|
return BuildResponse(r), nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return BuildResponse(r), &response, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client4) SelfHostedSignupConfirm(form *SelfHostedConfirmPaymentMethodRequest) (*Response, *SelfHostedSignupConfirmClientResponse, error) {
|
||||||
|
payloadBytes, err := json.Marshal(form)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, NewAppError("SelfHostedSignupConfirm", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
r, err := c.DoAPIPost(c.hostedCustomerRoute()+"/confirm", string(payloadBytes))
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return BuildResponse(r), nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := io.ReadAll(r.Body)
|
||||||
|
if err != nil {
|
||||||
|
return BuildResponse(r), nil, err
|
||||||
|
}
|
||||||
|
defer closeBody(r)
|
||||||
|
|
||||||
|
response := SelfHostedSignupConfirmClientResponse{}
|
||||||
|
err = json.Unmarshal(data, &response)
|
||||||
|
if err != nil {
|
||||||
|
return BuildResponse(r), nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
defer closeBody(r)
|
||||||
|
|
||||||
|
return BuildResponse(r), &response, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (c *Client4) GetPostInfo(postId string) (*PostInfo, *Response, error) {
|
func (c *Client4) GetPostInfo(postId string) (*PostInfo, *Response, error) {
|
||||||
r, err := c.DoAPIGet(c.postRoute(postId)+"/info", "")
|
r, err := c.DoAPIGet(c.postRoute(postId)+"/info", "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -290,17 +290,14 @@ type ProductLimits struct {
|
|||||||
Teams *TeamsLimits `json:"teams,omitempty"`
|
Teams *TeamsLimits `json:"teams,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type BootstrapSelfHostedSignupRequest struct {
|
// CreateSubscriptionRequest is the parameters for the API request to create a subscription.
|
||||||
Email string `json:"email"`
|
type CreateSubscriptionRequest struct {
|
||||||
}
|
ProductID string `json:"product_id"`
|
||||||
|
AddOns []string `json:"add_ons"`
|
||||||
type BootstrapSelfHostedSignupResponse struct {
|
Seats int `json:"seats"`
|
||||||
Progress string `json:"progress"`
|
Total float64 `json:"total"`
|
||||||
}
|
InternalPurchaseOrder string `json:"internal_purchase_order"`
|
||||||
|
DiscountID string `json:"discount_id"`
|
||||||
type BootstrapSelfHostedSignupResponseInternal struct {
|
|
||||||
Progress string `json:"progress"`
|
|
||||||
License string `json:"license"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Product) IsYearly() bool {
|
func (p *Product) IsYearly() bool {
|
||||||
|
|||||||
@@ -383,7 +383,7 @@ type ServiceSettings struct {
|
|||||||
CollapsedThreads *string `access:"experimental_features"`
|
CollapsedThreads *string `access:"experimental_features"`
|
||||||
ManagedResourcePaths *string `access:"environment_web_server,write_restrictable,cloud_restrictable"`
|
ManagedResourcePaths *string `access:"environment_web_server,write_restrictable,cloud_restrictable"`
|
||||||
EnableCustomGroups *bool `access:"site_users_and_teams"`
|
EnableCustomGroups *bool `access:"site_users_and_teams"`
|
||||||
SelfHostedFirstTimePurchase *bool `access:"write_restrictable,cloud_restrictable"`
|
SelfHostedPurchase *bool `access:"write_restrictable,cloud_restrictable"`
|
||||||
AllowSyncedDrafts *bool `access:"site_posts"`
|
AllowSyncedDrafts *bool `access:"site_posts"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -854,8 +854,8 @@ func (s *ServiceSettings) SetDefaults(isUpdate bool) {
|
|||||||
s.AllowSyncedDrafts = NewBool(true)
|
s.AllowSyncedDrafts = NewBool(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
if s.SelfHostedFirstTimePurchase == nil {
|
if s.SelfHostedPurchase == nil {
|
||||||
s.SelfHostedFirstTimePurchase = NewBool(false)
|
s.SelfHostedPurchase = NewBool(true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
58
model/hosted_customer.go
Обычный файл
58
model/hosted_customer.go
Обычный файл
@@ -0,0 +1,58 @@
|
|||||||
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
package model
|
||||||
|
|
||||||
|
type BootstrapSelfHostedSignupRequest struct {
|
||||||
|
Email string `json:"email"`
|
||||||
|
Reset bool `json:"reset"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type BootstrapSelfHostedSignupResponse struct {
|
||||||
|
Progress string `json:"progress"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type BootstrapSelfHostedSignupResponseInternal struct {
|
||||||
|
Progress string `json:"progress"`
|
||||||
|
License string `json:"license"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// email contained in token, so not in the request body.
|
||||||
|
type SelfHostedCustomerForm struct {
|
||||||
|
FirstName string `json:"first_name"`
|
||||||
|
LastName string `json:"last_name"`
|
||||||
|
BillingAddress *Address `json:"billing_address"`
|
||||||
|
Organization string `json:"organization"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type SelfHostedConfirmPaymentMethodRequest struct {
|
||||||
|
StripeSetupIntentID string `json:"stripe_setup_intent_id"`
|
||||||
|
Subscription CreateSubscriptionRequest `json:"subscription"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// SelfHostedSignupPaymentResponse contains feels needed for self hosted signup to confirm payment and receive license.
|
||||||
|
type SelfHostedSignupCustomerResponse struct {
|
||||||
|
CustomerId string `json:"customer_id"`
|
||||||
|
SetupIntentId string `json:"setup_intent_id"`
|
||||||
|
SetupIntentSecret string `json:"setup_intent_secret"`
|
||||||
|
Progress string `json:"progress"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// SelfHostedSignupConfirmResponse contains data received on successful self hosted signup
|
||||||
|
type SelfHostedSignupConfirmResponse struct {
|
||||||
|
License string `json:"license"`
|
||||||
|
Progress string `json:"progress"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type SelfHostedSignupConfirmClientResponse struct {
|
||||||
|
License map[string]string `json:"license"`
|
||||||
|
Progress string `json:"progress"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type SelfHostedBillingAccessRequest struct {
|
||||||
|
LicenseId string `json:"license_id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type SelfHostedBillingAccessResponse struct {
|
||||||
|
Token string `json:"token"`
|
||||||
|
}
|
||||||
@@ -81,6 +81,7 @@ const (
|
|||||||
WebsocketEventDraftDeleted = "draft_deleted"
|
WebsocketEventDraftDeleted = "draft_deleted"
|
||||||
WebsocketEventAcknowledgementAdded = "post_acknowledgement_added"
|
WebsocketEventAcknowledgementAdded = "post_acknowledgement_added"
|
||||||
WebsocketEventAcknowledgementRemoved = "post_acknowledgement_removed"
|
WebsocketEventAcknowledgementRemoved = "post_acknowledgement_removed"
|
||||||
|
WebsocketEventHostedCustomerSignupProgressUpdated = "hosted_customer_signup_progress_updated"
|
||||||
)
|
)
|
||||||
|
|
||||||
type WebSocketMessage interface {
|
type WebSocketMessage interface {
|
||||||
|
|||||||
@@ -455,7 +455,7 @@ func (ts *TelemetryService) trackConfig() {
|
|||||||
"restrict_link_previews": isDefault(*cfg.ServiceSettings.RestrictLinkPreviews, ""),
|
"restrict_link_previews": isDefault(*cfg.ServiceSettings.RestrictLinkPreviews, ""),
|
||||||
"enable_custom_groups": *cfg.ServiceSettings.EnableCustomGroups,
|
"enable_custom_groups": *cfg.ServiceSettings.EnableCustomGroups,
|
||||||
"post_priority": *cfg.ServiceSettings.PostPriority,
|
"post_priority": *cfg.ServiceSettings.PostPriority,
|
||||||
"self_hosted_first_time_purchase": *cfg.ServiceSettings.SelfHostedFirstTimePurchase,
|
"self_hosted_purchase": *cfg.ServiceSettings.SelfHostedPurchase,
|
||||||
"allow_synced_drafts": *cfg.ServiceSettings.AllowSyncedDrafts,
|
"allow_synced_drafts": *cfg.ServiceSettings.AllowSyncedDrafts,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -236,7 +236,7 @@ func (h Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
cloudCSP := ""
|
cloudCSP := ""
|
||||||
if c.App.Channels().License().IsCloud() || *c.App.Config().ServiceSettings.SelfHostedFirstTimePurchase {
|
if c.App.Channels().License().IsCloud() || *c.App.Config().ServiceSettings.SelfHostedPurchase {
|
||||||
cloudCSP = " js.stripe.com/v3"
|
cloudCSP = " js.stripe.com/v3"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -298,6 +298,29 @@ func TestHandlerServeCSPHeader(t *testing.T) {
|
|||||||
IsStatic: true,
|
IsStatic: true,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
request := httptest.NewRequest("POST", "/", nil)
|
||||||
|
response := httptest.NewRecorder()
|
||||||
|
handler.ServeHTTP(response, request)
|
||||||
|
assert.Equal(t, 200, response.Code)
|
||||||
|
assert.Equal(t, []string{"frame-ancestors 'self'; script-src 'self' cdn.rudderlabs.com js.stripe.com/v3"}, response.Header()["Content-Security-Policy"])
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("static, without subpath or SelfHostedPurchase, does not allow Stripe in CSP", func(t *testing.T) {
|
||||||
|
th := Setup(t).InitBasic()
|
||||||
|
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.SelfHostedPurchase = false })
|
||||||
|
defer th.TearDown()
|
||||||
|
|
||||||
|
web := New(th.Server)
|
||||||
|
|
||||||
|
handler := Handler{
|
||||||
|
Srv: web.srv,
|
||||||
|
HandleFunc: handlerForCSPHeader,
|
||||||
|
RequireSession: false,
|
||||||
|
TrustRequester: false,
|
||||||
|
RequireMfa: false,
|
||||||
|
IsStatic: true,
|
||||||
|
}
|
||||||
|
|
||||||
request := httptest.NewRequest("POST", "/", nil)
|
request := httptest.NewRequest("POST", "/", nil)
|
||||||
response := httptest.NewRecorder()
|
response := httptest.NewRecorder()
|
||||||
handler.ServeHTTP(response, request)
|
handler.ServeHTTP(response, request)
|
||||||
@@ -343,7 +366,7 @@ func TestHandlerServeCSPHeader(t *testing.T) {
|
|||||||
response := httptest.NewRecorder()
|
response := httptest.NewRecorder()
|
||||||
handler.ServeHTTP(response, request)
|
handler.ServeHTTP(response, request)
|
||||||
assert.Equal(t, 200, response.Code)
|
assert.Equal(t, 200, response.Code)
|
||||||
assert.Equal(t, []string{"frame-ancestors 'self'; script-src 'self' cdn.rudderlabs.com"}, response.Header()["Content-Security-Policy"])
|
assert.Equal(t, []string{"frame-ancestors 'self'; script-src 'self' cdn.rudderlabs.com js.stripe.com/v3"}, response.Header()["Content-Security-Policy"])
|
||||||
|
|
||||||
// TODO: It's hard to unit test this now that the CSP directive is effectively
|
// TODO: It's hard to unit test this now that the CSP directive is effectively
|
||||||
// decided in Setup(). Circle back to this in master once the memory store is
|
// decided in Setup(). Circle back to this in master once the memory store is
|
||||||
@@ -358,7 +381,7 @@ func TestHandlerServeCSPHeader(t *testing.T) {
|
|||||||
response = httptest.NewRecorder()
|
response = httptest.NewRecorder()
|
||||||
handler.ServeHTTP(response, request)
|
handler.ServeHTTP(response, request)
|
||||||
assert.Equal(t, 200, response.Code)
|
assert.Equal(t, 200, response.Code)
|
||||||
assert.Equal(t, []string{"frame-ancestors 'self'; script-src 'self' cdn.rudderlabs.com"}, response.Header()["Content-Security-Policy"])
|
assert.Equal(t, []string{"frame-ancestors 'self'; script-src 'self' cdn.rudderlabs.com js.stripe.com/v3"}, response.Header()["Content-Security-Policy"])
|
||||||
// TODO: See above.
|
// TODO: See above.
|
||||||
// assert.Contains(t, response.Header()["Content-Security-Policy"], "frame-ancestors 'self'; script-src 'self' cdn.rudderlabs.com 'sha256-tPOjw+tkVs9axL78ZwGtYl975dtyPHB6LYKAO2R3gR4='", "csp header incorrectly changed after subpath changed")
|
// assert.Contains(t, response.Header()["Content-Security-Policy"], "frame-ancestors 'self'; script-src 'self' cdn.rudderlabs.com 'sha256-tPOjw+tkVs9axL78ZwGtYl975dtyPHB6LYKAO2R3gR4='", "csp header incorrectly changed after subpath changed")
|
||||||
})
|
})
|
||||||
@@ -388,7 +411,7 @@ func TestHandlerServeCSPHeader(t *testing.T) {
|
|||||||
response := httptest.NewRecorder()
|
response := httptest.NewRecorder()
|
||||||
handler.ServeHTTP(response, request)
|
handler.ServeHTTP(response, request)
|
||||||
assert.Equal(t, 200, response.Code)
|
assert.Equal(t, 200, response.Code)
|
||||||
assert.Equal(t, []string{"frame-ancestors 'self'; script-src 'self' cdn.rudderlabs.com 'unsafe-eval' 'unsafe-inline' http://localhost:9006"}, response.Header()["Content-Security-Policy"])
|
assert.Equal(t, []string{"frame-ancestors 'self'; script-src 'self' cdn.rudderlabs.com js.stripe.com/v3 'unsafe-eval' 'unsafe-inline' http://localhost:9006"}, response.Header()["Content-Security-Policy"])
|
||||||
})
|
})
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
Ссылка в новой задаче
Block a user