diff --git a/api4/cloud.go b/api4/cloud.go index 62d9a1350f..3660282960 100644 --- a/api4/cloud.go +++ b/api4/cloud.go @@ -44,6 +44,9 @@ func (api *API) InitCloud() { // GET /api/v4/cloud/request-trial api.BaseRoutes.Cloud.Handle("/request-trial", api.APISessionRequired(requestCloudTrial)).Methods("PUT") + // GET /api/v4/cloud/validate-business-email + api.BaseRoutes.Cloud.Handle("/validate-business-email", api.APISessionRequired(validateBusinessEmail)).Methods("POST") + // POST /api/v4/cloud/webhook api.BaseRoutes.Cloud.Handle("/webhook", api.CloudAPIKeyRequired(handleCWSWebhook)).Methods("POST") } @@ -146,7 +149,21 @@ func requestCloudTrial(c *Context, w http.ResponseWriter, r *http.Request) { return } - changedSub, err := c.App.Cloud().RequestCloudTrial(c.AppContext.Session().UserId, currentSubscription.ID) + // check if the email needs to be set + bodyBytes, err := ioutil.ReadAll(r.Body) + if err != nil { + c.Err = model.NewAppError("Api4.requestCloudTrial", "api.cloud.app_error", nil, err.Error(), http.StatusInternalServerError) + return + } + // this value will not be empty when both emails (user admin and CWS customer) are not business email and + // we need to request a new email from the user via the request business email modal + var newValidBusinessEmail *model.ValidateBusinessEmailRequest + if err = json.Unmarshal(bodyBytes, &newValidBusinessEmail); err != nil { + c.Err = model.NewAppError("Api4.requestCloudTrial", "api.cloud.app_error", nil, err.Error(), http.StatusInternalServerError) + return + } + + changedSub, err := c.App.Cloud().RequestCloudTrial(c.AppContext.Session().UserId, currentSubscription.ID, newValidBusinessEmail.Email) if err != nil { c.Err = model.NewAppError("Api4.requestCloudTrial", "api.cloud.app_error", nil, err.Error(), http.StatusInternalServerError) return @@ -161,6 +178,48 @@ func requestCloudTrial(c *Context, w http.ResponseWriter, r *http.Request) { w.Write(json) } +func validateBusinessEmail(c *Context, w http.ResponseWriter, r *http.Request) { + if c.App.Channels().License() == nil || !*c.App.Channels().License().Features.Cloud { + c.Err = model.NewAppError("Api4.validateBusinessEmail", "api.cloud.license_error", nil, "", http.StatusForbidden) + return + } + + if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleWriteBilling) { + c.SetPermissionError(model.PermissionSysconsoleWriteBilling) + return + } + + user, userErr := c.App.GetUser(c.AppContext.Session().UserId) + if userErr != nil { + c.Err = model.NewAppError("Api4.validateBusinessEmail", "api.cloud.request_error", nil, "", http.StatusInternalServerError) + return + } + + // validate current userAdmin email + errValidatingAdminEmail := c.App.Cloud().ValidateBusinessEmail(user.Id, user.Email) + + // if the current admin email is not a valid email + if errValidatingAdminEmail != nil { + + // get the cloud customer email + cloudCustomer, err := c.App.Cloud().GetCloudCustomer(user.Id) + if err != nil { + c.Err = model.NewAppError("Api4.valiateBusinessEmail", "api.cloud.request_error", nil, err.Error(), http.StatusInternalServerError) + return + } + + // and validate that one + errValidatingSystemEmail := c.App.Cloud().ValidateBusinessEmail(user.Id, cloudCustomer.Email) + if errValidatingSystemEmail != nil { + c.Err = model.NewAppError("Api4.valiateBusinessEmail", "api.cloud.request_error", nil, errValidatingSystemEmail.Error(), http.StatusInternalServerError) + return + } + } + + // if the email is valid, return ok + ReturnStatusOK(w) +} + func getCloudProducts(c *Context, w http.ResponseWriter, r *http.Request) { if c.App.Channels().License() == nil || !*c.App.Channels().License().Features.Cloud { c.Err = model.NewAppError("Api4.getCloudProducts", "api.cloud.license_error", nil, "", http.StatusNotImplemented) diff --git a/api4/cloud_test.go b/api4/cloud_test.go index 5a9594bf51..e360174872 100644 --- a/api4/cloud_test.go +++ b/api4/cloud_test.go @@ -6,6 +6,7 @@ package api4 import ( "errors" "net/http" + "net/http/httptest" "os" "testing" @@ -156,6 +157,8 @@ func Test_requestTrial(t *testing.T) { IsPaidTier: "false", } + newValidBusinessEmail := model.ValidateBusinessEmailRequest{Email: ""} + t.Run("NON Admin users are UNABLE to request the trial", func(t *testing.T) { th := Setup(t).InitBasic() defer th.TearDown() @@ -171,7 +174,7 @@ func Test_requestTrial(t *testing.T) { cloud := mocks.CloudInterface{} cloud.Mock.On("GetSubscription", mock.Anything).Return(subscription, nil) - cloud.Mock.On("RequestCloudTrial", mock.Anything, mock.Anything).Return(subscription, nil) + cloud.Mock.On("RequestCloudTrial", mock.Anything, mock.Anything, "").Return(subscription, nil) cloudImpl := th.App.Srv().Cloud defer func() { @@ -179,8 +182,7 @@ func Test_requestTrial(t *testing.T) { }() th.App.Srv().Cloud = &cloud - subscriptionChanged, r, err := th.Client.RequestCloudTrial() - t.Logf("\n\nresp %#v, \n\n r: %v\n\n, err: %v\n\n", subscriptionChanged, r, err) + subscriptionChanged, r, err := th.Client.RequestCloudTrial(&newValidBusinessEmail) require.Error(t, err) require.Nil(t, subscriptionChanged) require.Equal(t, http.StatusForbidden, r.StatusCode, "403 Forbidden") @@ -201,7 +203,7 @@ func Test_requestTrial(t *testing.T) { cloud := mocks.CloudInterface{} cloud.Mock.On("GetSubscription", mock.Anything).Return(subscription, nil) - cloud.Mock.On("RequestCloudTrial", mock.Anything, mock.Anything).Return(subscription, nil) + cloud.Mock.On("RequestCloudTrial", mock.Anything, mock.Anything, "").Return(subscription, nil) cloudImpl := th.App.Srv().Cloud defer func() { @@ -209,7 +211,7 @@ func Test_requestTrial(t *testing.T) { }() th.App.Srv().Cloud = &cloud - subscriptionChanged, r, err := th.SystemAdminClient.RequestCloudTrial() + subscriptionChanged, r, err := th.SystemAdminClient.RequestCloudTrial(&newValidBusinessEmail) require.Error(t, err) require.Nil(t, subscriptionChanged) @@ -231,7 +233,7 @@ func Test_requestTrial(t *testing.T) { cloud := mocks.CloudInterface{} cloud.Mock.On("GetSubscription", mock.Anything).Return(subscription, nil) - cloud.Mock.On("RequestCloudTrial", mock.Anything, mock.Anything).Return(subscription, nil) + cloud.Mock.On("RequestCloudTrial", mock.Anything, mock.Anything, "").Return(subscription, nil) cloudImpl := th.App.Srv().Cloud defer func() { @@ -239,10 +241,69 @@ func Test_requestTrial(t *testing.T) { }() th.App.Srv().Cloud = &cloud - subscriptionChanged, r, err := th.SystemAdminClient.RequestCloudTrial() + subscriptionChanged, r, err := th.SystemAdminClient.RequestCloudTrial(&newValidBusinessEmail) + + require.NoError(t, err) + require.Equal(t, subscriptionChanged, subscription) + require.Equal(t, http.StatusOK, r.StatusCode, "Status OK") + }) + + t.Run("cloudFree feature flag TRUE and ADMIN user are ABLE to request the trial with valid business email", func(t *testing.T) { + th := Setup(t).InitBasic() + defer th.TearDown() + + // patch the customer with the additional contact updated with the valid business email + newValidBusinessEmail.Email = *model.NewString("valid.email@mattermost.com") + + os.Setenv("MM_FEATUREFLAGS_CLOUDFREE", "true") + defer os.Unsetenv("MM_FEATUREFLAGS_CLOUDFREE") + th.App.ReloadConfig() + + th.Client.Login(th.BasicUser.Email, th.BasicUser.Password) + + th.App.Srv().SetLicense(model.NewTestLicense("cloud")) + + cloud := mocks.CloudInterface{} + + cloud.Mock.On("GetSubscription", mock.Anything).Return(subscription, nil) + cloud.Mock.On("RequestCloudTrial", mock.Anything, mock.Anything, "valid.email@mattermost.com").Return(subscription, nil) + + cloudImpl := th.App.Srv().Cloud + defer func() { + th.App.Srv().Cloud = cloudImpl + }() + th.App.Srv().Cloud = &cloud + + subscriptionChanged, r, err := th.SystemAdminClient.RequestCloudTrial(&newValidBusinessEmail) require.NoError(t, err) require.Equal(t, subscriptionChanged, subscription) require.Equal(t, http.StatusOK, r.StatusCode, "Status OK") }) } + +func Test_validateBusinessEmail(t *testing.T) { + t.Run("Initial request has invalid email", func(t *testing.T) { + th := Setup(t).InitBasic() + defer th.TearDown() + + th.Client.Login(th.BasicUser.Email, th.BasicUser.Password) + + th.App.Srv().SetLicense(model.NewTestLicense("cloud")) + + cloud := mocks.CloudInterface{} + + resp := httptest.NewRecorder() + + cloud.Mock.On("ValidateBusinessEmail", mock.Anything).Return(resp, nil) + + cloudImpl := th.App.Srv().Cloud + defer func() { + th.App.Srv().Cloud = cloudImpl + }() + th.App.Srv().Cloud = &cloud + + _, err := th.Client.ValidateBusinessEmail() + require.Error(t, err) + }) +} diff --git a/einterfaces/cloud.go b/einterfaces/cloud.go index a403e9ba2c..26e7f3c5ab 100644 --- a/einterfaces/cloud.go +++ b/einterfaces/cloud.go @@ -25,7 +25,8 @@ type CloudInterface interface { ChangeSubscription(userID, subscriptionID string, subscriptionChange *model.SubscriptionChange) (*model.Subscription, error) - RequestCloudTrial(userID, subscriptionID string) (*model.Subscription, error) + RequestCloudTrial(userID, subscriptionID, newValidBusinessEmail string) (*model.Subscription, error) + ValidateBusinessEmail(userID, email string) error // GetLicenseRenewalStatus checks on the portal whether it is possible to use token to renew a license GetLicenseRenewalStatus(userID, token string) error diff --git a/einterfaces/mocks/CloudInterface.go b/einterfaces/mocks/CloudInterface.go index 301918b613..1e5ba2b0a1 100644 --- a/einterfaces/mocks/CloudInterface.go +++ b/einterfaces/mocks/CloudInterface.go @@ -247,13 +247,13 @@ func (_m *CloudInterface) InvalidateCaches() error { return r0 } -// RequestCloudTrial provides a mock function with given fields: userID, subscriptionID -func (_m *CloudInterface) RequestCloudTrial(userID string, subscriptionID string) (*model.Subscription, error) { - ret := _m.Called(userID, subscriptionID) +// RequestCloudTrial provides a mock function with given fields: userID, subscriptionID, newValidBusinessEmail +func (_m *CloudInterface) RequestCloudTrial(userID string, subscriptionID string, newValidBusinessEmail string) (*model.Subscription, error) { + ret := _m.Called(userID, subscriptionID, newValidBusinessEmail) var r0 *model.Subscription - if rf, ok := ret.Get(0).(func(string, string) *model.Subscription); ok { - r0 = rf(userID, subscriptionID) + if rf, ok := ret.Get(0).(func(string, string, string) *model.Subscription); ok { + r0 = rf(userID, subscriptionID, newValidBusinessEmail) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(*model.Subscription) @@ -261,8 +261,8 @@ func (_m *CloudInterface) RequestCloudTrial(userID string, subscriptionID string } var r1 error - if rf, ok := ret.Get(1).(func(string, string) error); ok { - r1 = rf(userID, subscriptionID) + if rf, ok := ret.Get(1).(func(string, string, string) error); ok { + r1 = rf(userID, subscriptionID, newValidBusinessEmail) } else { r1 = ret.Error(1) } @@ -329,3 +329,17 @@ func (_m *CloudInterface) UpdateSubscriptionFromHook(_a0 *model.ProductLimits, _ return r0 } + +// ValidateBusinessEmail provides a mock function with given fields: userID, email +func (_m *CloudInterface) ValidateBusinessEmail(userID string, email string) error { + ret := _m.Called(userID, email) + + var r0 error + if rf, ok := ret.Get(0).(func(string, string) error); ok { + r0 = rf(userID, email) + } else { + r0 = ret.Error(0) + } + + return r0 +} diff --git a/model/client4.go b/model/client4.go index b0d2144e6a..27d7bdb62a 100644 --- a/model/client4.go +++ b/model/client4.go @@ -7784,8 +7784,9 @@ func (c *Client4) ConfirmCustomerPayment(confirmRequest *ConfirmPaymentMethodReq return BuildResponse(r), nil } -func (c *Client4) RequestCloudTrial() (*Subscription, *Response, error) { - r, err := c.DoAPIPut(c.cloudRoute()+"/request-trial", "") +func (c *Client4) RequestCloudTrial(email *ValidateBusinessEmailRequest) (*Subscription, *Response, error) { + payload, _ := json.Marshal(email) + r, err := c.DoAPIPutBytes(c.cloudRoute()+"/request-trial", payload) if err != nil { return nil, BuildResponse(r), err } @@ -7797,6 +7798,16 @@ func (c *Client4) RequestCloudTrial() (*Subscription, *Response, error) { return subscription, BuildResponse(r), nil } +func (c *Client4) ValidateBusinessEmail() (*Response, error) { + r, err := c.DoAPIPost(c.cloudRoute()+"/validate-business-email", "") + if err != nil { + return BuildResponse(r), err + } + defer closeBody(r) + + return BuildResponse(r), nil +} + func (c *Client4) GetCloudCustomer() (*CloudCustomer, *Response, error) { r, err := c.DoAPIGet(c.cloudRoute()+"/customer", "") if err != nil { diff --git a/model/cloud.go b/model/cloud.go index 24ca1fb029..1c10c991c0 100644 --- a/model/cloud.go +++ b/model/cloud.go @@ -84,6 +84,10 @@ type CloudCustomer struct { PaymentMethod *PaymentMethod `json:"payment_method"` } +type ValidateBusinessEmailRequest struct { + Email string `json:"email"` +} + // CloudCustomerInfo represents editable info of a customer. type CloudCustomerInfo struct { Name string `json:"name"`