Skip to content

Commit ca08652

Browse files
authored
Installer: Fix issues with newsletter signup (#20705)
* Add setter to allow handling of requests to subscribe to newsletter on install. * Correct serialization of newsletter subscription request. * Fix serialization and use the Umbraco.EmailMarketing service for newsletter signup. * Remove logging of user when setting telemetry level. * Applied suggestions from code review.
1 parent b866c31 commit ca08652

File tree

3 files changed

+113
-50
lines changed

3 files changed

+113
-50
lines changed

src/Umbraco.Cms.Api.Management/ViewModels/Installer/UserInstallRequestModel.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
using System.ComponentModel;
1+
using System.ComponentModel;
22
using System.ComponentModel.DataAnnotations;
33

44
namespace Umbraco.Cms.Api.Management.ViewModels.Installer;
@@ -17,5 +17,5 @@ public class UserInstallRequestModel
1717
[PasswordPropertyText]
1818
public string Password { get; set; } = string.Empty;
1919

20-
public bool SubscribeToNewsletter { get; }
20+
public bool SubscribeToNewsletter { get; set; }
2121
}

src/Umbraco.Core/Services/MetricsConsentService.cs

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -39,11 +39,10 @@ public TelemetryLevel GetConsentLevel()
3939
return analyticsLevel;
4040
}
4141

42-
public async Task SetConsentLevelAsync(TelemetryLevel telemetryLevel)
42+
public Task SetConsentLevelAsync(TelemetryLevel telemetryLevel)
4343
{
44-
IUser? currentUser = _backOfficeSecurityAccessor.BackOfficeSecurity?.CurrentUser ?? await _userService.GetAsync(Constants.Security.SuperUserKey);
45-
46-
_logger.LogInformation("Telemetry level set to {telemetryLevel} by {username}", telemetryLevel, currentUser?.Username);
44+
_logger.LogInformation("Telemetry level set to {telemetryLevel}", telemetryLevel);
4745
_keyValueService.SetValue(Key, telemetryLevel.ToString());
46+
return Task.CompletedTask;
4847
}
4948
}

src/Umbraco.Infrastructure/Installer/Steps/CreateUserStep.cs

Lines changed: 108 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
1-
using System.Collections.Specialized;
21
using System.Data.Common;
3-
using System.Text;
42
using Microsoft.AspNetCore.Identity;
3+
using Microsoft.Extensions.DependencyInjection;
4+
using Microsoft.Extensions.Logging;
55
using Microsoft.Extensions.Options;
66
using Umbraco.Cms.Core;
77
using Umbraco.Cms.Core.Configuration.Models;
8+
using Umbraco.Cms.Core.DependencyInjection;
89
using Umbraco.Cms.Core.Installer;
910
using Umbraco.Cms.Core.Models.Installer;
1011
using Umbraco.Cms.Core.Models.Membership;
@@ -32,7 +33,9 @@ public class CreateUserStep : StepBase, IInstallStep
3233
private readonly IDbProviderFactoryCreator _dbProviderFactoryCreator;
3334
private readonly IMetricsConsentService _metricsConsentService;
3435
private readonly IJsonSerializer _jsonSerializer;
36+
private readonly ILogger<CreateUserStep> _logger;
3537

38+
[Obsolete("Please use the constructor that takes all parameters. Scheduled for removal in Umbraco 19.")]
3639
public CreateUserStep(
3740
IUserService userService,
3841
DatabaseBuilder databaseBuilder,
@@ -44,71 +47,132 @@ public CreateUserStep(
4447
IDbProviderFactoryCreator dbProviderFactoryCreator,
4548
IMetricsConsentService metricsConsentService,
4649
IJsonSerializer jsonSerializer)
50+
: this(
51+
userService,
52+
databaseBuilder,
53+
httpClientFactory,
54+
securitySettings,
55+
connectionStrings,
56+
cookieManager,
57+
userManager,
58+
dbProviderFactoryCreator,
59+
metricsConsentService,
60+
jsonSerializer,
61+
StaticServiceProvider.Instance.GetRequiredService<ILogger<CreateUserStep>>())
4762
{
48-
_userService = userService ?? throw new ArgumentNullException(nameof(userService));
49-
_databaseBuilder = databaseBuilder ?? throw new ArgumentNullException(nameof(databaseBuilder));
63+
}
64+
65+
public CreateUserStep(
66+
IUserService userService,
67+
DatabaseBuilder databaseBuilder,
68+
IHttpClientFactory httpClientFactory,
69+
IOptions<SecuritySettings> securitySettings,
70+
IOptionsMonitor<ConnectionStrings> connectionStrings,
71+
ICookieManager cookieManager,
72+
IBackOfficeUserManager userManager,
73+
IDbProviderFactoryCreator dbProviderFactoryCreator,
74+
IMetricsConsentService metricsConsentService,
75+
IJsonSerializer jsonSerializer,
76+
ILogger<CreateUserStep> logger)
77+
{
78+
_userService = userService;
79+
_databaseBuilder = databaseBuilder;
5080
_httpClientFactory = httpClientFactory;
51-
_securitySettings = securitySettings.Value ?? throw new ArgumentNullException(nameof(securitySettings));
81+
_securitySettings = securitySettings.Value;
5282
_connectionStrings = connectionStrings;
5383
_cookieManager = cookieManager;
54-
_userManager = userManager ?? throw new ArgumentNullException(nameof(userManager));
55-
_dbProviderFactoryCreator = dbProviderFactoryCreator ?? throw new ArgumentNullException(nameof(dbProviderFactoryCreator));
84+
_userManager = userManager;
85+
_dbProviderFactoryCreator = dbProviderFactoryCreator;
5686
_metricsConsentService = metricsConsentService;
5787
_jsonSerializer = jsonSerializer;
88+
_logger = logger;
5889
}
5990

6091
public async Task<Attempt<InstallationResult>> ExecuteAsync(InstallData model)
6192
{
62-
IUser? admin = _userService.GetAsync(Constants.Security.SuperUserKey).GetAwaiter().GetResult();
63-
if (admin is null)
64-
{
65-
return FailWithMessage("Could not find the super user");
66-
}
93+
IUser? admin = await _userService.GetAsync(Constants.Security.SuperUserKey);
94+
if (admin is null)
95+
{
96+
return FailWithMessage("Could not find the super user");
97+
}
6798

68-
UserInstallData user = model.User;
69-
admin.Email = user.Email.Trim();
70-
admin.Name = user.Name.Trim();
71-
admin.Username = user.Email.Trim();
99+
UserInstallData user = model.User;
100+
admin.Email = user.Email.Trim();
101+
admin.Name = user.Name.Trim();
102+
admin.Username = user.Email.Trim();
72103

73-
_userService.Save(admin);
104+
_userService.Save(admin);
74105

75-
BackOfficeIdentityUser? membershipUser = await _userManager.FindByIdAsync(Constants.Security.SuperUserIdAsString);
76-
if (membershipUser == null)
77-
{
78-
return FailWithMessage(
79-
$"No user found in membership provider with id of {Constants.Security.SuperUserIdAsString}.");
80-
}
106+
BackOfficeIdentityUser? membershipUser = await _userManager.FindByIdAsync(Constants.Security.SuperUserIdAsString);
107+
if (membershipUser == null)
108+
{
109+
return FailWithMessage(
110+
$"No user found in membership provider with id of {Constants.Security.SuperUserIdAsString}.");
111+
}
81112

82-
// To change the password here we actually need to reset it since we don't have an old one to use to change
83-
var resetToken = await _userManager.GeneratePasswordResetTokenAsync(membershipUser);
84-
if (string.IsNullOrWhiteSpace(resetToken))
113+
// To change the password here we actually need to reset it since we don't have an old one to use to change
114+
var resetToken = await _userManager.GeneratePasswordResetTokenAsync(membershipUser);
115+
if (string.IsNullOrWhiteSpace(resetToken))
116+
{
117+
return FailWithMessage("Could not reset password: unable to generate internal reset token");
118+
}
119+
120+
IdentityResult resetResult = await _userManager.ChangePasswordWithResetAsync(membershipUser.Id, resetToken, user.Password.Trim());
121+
if (!resetResult.Succeeded)
122+
{
123+
return FailWithMessage("Could not reset password: " + string.Join(", ", resetResult.Errors.ToErrorMessage()));
124+
}
125+
126+
await _metricsConsentService.SetConsentLevelAsync(model.TelemetryLevel);
127+
128+
if (model.User.SubscribeToNewsletter)
129+
{
130+
const string EmailCollectorUrl = "https://emailcollector.umbraco.io/api/EmailProxy";
131+
132+
var emailModel = new EmailModel
133+
{
134+
Name = admin.Name,
135+
Email = admin.Email,
136+
UserGroup = [Constants.Security.AdminGroupAlias],
137+
};
138+
139+
HttpClient httpClient = _httpClientFactory.CreateClient();
140+
using var content = new StringContent(_jsonSerializer.Serialize(emailModel), System.Text.Encoding.UTF8, "application/json");
141+
try
85142
{
86-
return FailWithMessage("Could not reset password: unable to generate internal reset token");
143+
// Set a reasonable timeout of 5 seconds for web request to save subscriber.
144+
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
145+
HttpResponseMessage response = await httpClient.PostAsync(EmailCollectorUrl, content, cts.Token);
146+
if (response.IsSuccessStatusCode)
147+
{
148+
_logger.LogInformation("Successfully subscribed the user created on installation to the Umbraco newsletter.");
149+
}
150+
else
151+
{
152+
_logger.LogWarning("Failed to subscribe the user created on installation to the Umbraco newsletter. Status code: {StatusCode}", response.StatusCode);
153+
}
87154
}
88-
89-
IdentityResult resetResult = await _userManager.ChangePasswordWithResetAsync(membershipUser.Id, resetToken, user.Password.Trim());
90-
if (!resetResult.Succeeded)
155+
catch (Exception ex)
91156
{
92-
return FailWithMessage("Could not reset password: " + string.Join(", ", resetResult.Errors.ToErrorMessage()));
157+
// Log and move on if a failure occurs, we don't want to block installation for this.
158+
_logger.LogError(ex, "Exception occurred while trying to subscribe the user created on installation to the Umbraco newsletter.");
93159
}
94160

95-
await _metricsConsentService.SetConsentLevelAsync(model.TelemetryLevel);
161+
}
96162

97-
if (model.User.SubscribeToNewsletter)
98-
{
99-
var values = new NameValueCollection { { "name", admin.Name }, { "email", admin.Email } };
100-
var content = new StringContent(_jsonSerializer.Serialize(values), Encoding.UTF8, "application/json");
163+
return Success();
164+
}
101165

102-
HttpClient httpClient = _httpClientFactory.CreateClient();
166+
/// <summary>
167+
/// Model used to subscribe to the newsletter. Aligns with EmailModel defined in Umbraco.EmailMarketing.
168+
/// </summary>
169+
private class EmailModel
170+
{
171+
public required string Name { get; init; }
103172

104-
try
105-
{
106-
HttpResponseMessage response = httpClient.PostAsync("https://shop.umbraco.com/base/Ecom/SubmitEmail/installer.aspx", content).Result;
107-
}
108-
catch { /* fail in silence */ }
109-
}
173+
public required string Email { get; init; }
110174

111-
return Success();
175+
public required List<string> UserGroup { get; init; }
112176
}
113177

114178
/// <inheritdoc/>

0 commit comments

Comments
 (0)