Skip to content
This repository was archived by the owner on Jul 30, 2024. It is now read-only.

Commit 31c4e6c

Browse files
authored
Merge branch 'dev' into master
2 parents ada7125 + b5c8139 commit 31c4e6c

69 files changed

Lines changed: 4268 additions & 310 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.gitignore

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ nuget.exe
77
*.suo
88
*.user
99
*.sln.docstates
10+
.vs/config/applicationhost.config
1011

1112
# Build results
1213
[Dd]ebug/
@@ -184,6 +185,7 @@ UpgradeLog*.htm
184185
# SQL Server files
185186
*.mdf
186187
*.ldf
188+
*.jfm
187189

188190
# Business Intelligence projects
189191
*.rdl.data
@@ -196,4 +198,4 @@ FakesAssemblies/
196198

197199
# Vs2015
198200
.vs/config/applicationhost.config
199-
*.jfm
201+
project.lock.json

NuGet.Jobs.sln

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,10 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Gallery", "Gallery", "{8872
7575
EndProject
7676
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Gallery.CredentialExpiration", "src\Gallery.CredentialExpiration\Gallery.CredentialExpiration.csproj", "{FA8C7905-985F-4919-AAA9-4B9A252F4977}"
7777
EndProject
78+
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "SupportRequests", "SupportRequests", "{BEC3DF4D-9A04-42C8-8B4F-D42750202B4D}"
79+
EndProject
80+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NuGet.SupportRequests.Notifications", "src\NuGet.SupportRequests.Notifications\NuGet.SupportRequests.Notifications.csproj", "{12719498-B87E-4E92-8C2B-30046393CF85}"
81+
EndProject
7882
Global
7983
GlobalSection(SolutionConfigurationPlatforms) = preSolution
8084
Debug|Any CPU = Debug|Any CPU
@@ -179,6 +183,10 @@ Global
179183
{FA8C7905-985F-4919-AAA9-4B9A252F4977}.Debug|Any CPU.Build.0 = Debug|Any CPU
180184
{FA8C7905-985F-4919-AAA9-4B9A252F4977}.Release|Any CPU.ActiveCfg = Release|Any CPU
181185
{FA8C7905-985F-4919-AAA9-4B9A252F4977}.Release|Any CPU.Build.0 = Release|Any CPU
186+
{12719498-B87E-4E92-8C2B-30046393CF85}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
187+
{12719498-B87E-4E92-8C2B-30046393CF85}.Debug|Any CPU.Build.0 = Debug|Any CPU
188+
{12719498-B87E-4E92-8C2B-30046393CF85}.Release|Any CPU.ActiveCfg = Release|Any CPU
189+
{12719498-B87E-4E92-8C2B-30046393CF85}.Release|Any CPU.Build.0 = Release|Any CPU
182190
EndGlobalSection
183191
GlobalSection(SolutionProperties) = preSolution
184192
HideSolutionNode = FALSE
@@ -208,5 +216,6 @@ Global
208216
{185EF6D4-2172-40B1-A80E-811CE9D85840} = {678D7B14-F8BC-4193-99AF-2EE8AA390A02}
209217
{1EB7FF94-9B4A-4008-8F8E-5F867C0B00DE} = {678D7B14-F8BC-4193-99AF-2EE8AA390A02}
210218
{FA8C7905-985F-4919-AAA9-4B9A252F4977} = {88725659-D5F8-49F9-9B7E-D87C5B9917D7}
219+
{12719498-B87E-4E92-8C2B-30046393CF85} = {BEC3DF4D-9A04-42C8-8B4F-D42750202B4D}
211220
EndGlobalSection
212221
EndGlobal

src/Gallery.CredentialExpiration/Gallery.CredentialExpiration.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,7 @@
9696
<Reference Include="System.Xml" />
9797
</ItemGroup>
9898
<ItemGroup>
99+
<Compile Include="LogEvents.cs" />
99100
<Compile Include="MyJobArgumentNames.cs" />
100101
<Compile Include="Models\ExpiredCredentialData.cs" />
101102
<Compile Include="Job.cs" />

src/Gallery.CredentialExpiration/Job.cs

Lines changed: 82 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,9 @@ public class Job : JobBase
3535
private string _mailFrom;
3636
private SmtpClient _smtpClient;
3737

38+
private int _allowEmailResendAfterDays = 7;
3839
private int _warnDaysBeforeExpiration = 10;
39-
40+
4041
private ILogger _logger;
4142

4243
public override bool Init(IDictionary<string, string> jobArgsDictionary)
@@ -64,15 +65,15 @@ public override bool Init(IDictionary<string, string> jobArgsDictionary)
6465
var smtpUri = new SmtpUri(new Uri(smtpConnectionString));
6566
_smtpClient = CreateSmtpClient(smtpUri);
6667

67-
var temp = JobConfigurationManager.TryGetIntArgument(jobArgsDictionary, MyJobArgumentNames.WarnDaysBeforeExpiration);
68-
if (temp.HasValue)
69-
{
70-
_warnDaysBeforeExpiration = temp.Value;
71-
}
68+
_warnDaysBeforeExpiration = JobConfigurationManager.TryGetIntArgument(jobArgsDictionary, MyJobArgumentNames.WarnDaysBeforeExpiration)
69+
?? _warnDaysBeforeExpiration;
70+
71+
_allowEmailResendAfterDays = JobConfigurationManager.TryGetIntArgument(jobArgsDictionary, MyJobArgumentNames.AllowEmailResendAfterDays)
72+
?? _allowEmailResendAfterDays;
7273
}
7374
catch (Exception exception)
7475
{
75-
_logger.LogCritical("Failed to initialize job! {Exception}", exception);
76+
_logger.LogCritical(LogEvents.JobInitFailed, exception, "Failed to initialize job!");
7677

7778
return false;
7879
}
@@ -93,8 +94,8 @@ public override async Task<bool> Run()
9394
var contactedUsers = JsonConvert.DeserializeObject<Dictionary<string, DateTimeOffset>>(
9495
File.ReadAllText(_cursorFile));
9596

96-
// Clean older entries (contacted in last _warnDaysBeforeExpiration * 2 days)
97-
var referenceDate = DateTimeOffset.UtcNow.AddDays(-2 * _warnDaysBeforeExpiration);
97+
// Clean older entries (contacted in last _allowEmailResendAfterDays)
98+
var referenceDate = DateTimeOffset.UtcNow.AddDays(-1 * _allowEmailResendAfterDays);
9899
foreach (var kvp in contactedUsers.Where(kvp => kvp.Value >= referenceDate))
99100
{
100101
_contactedUsers.AddOrUpdate(kvp.Key, kvp.Value, (s, offset) => kvp.Value);
@@ -118,24 +119,51 @@ public override async Task<bool> Run()
118119
expiredCredentials.Count);
119120
}
120121

122+
// Add default description for non-scoped API keys
123+
expiredCredentials
124+
.Where(cred => string.IsNullOrEmpty(cred.Description))
125+
.ToList()
126+
.ForEach(ecd => ecd.Description = Constants.NonScopedApiKeyDescription);
127+
128+
// Group credentials for each user
129+
var userToExpiredCredsMapping = expiredCredentials
130+
.GroupBy(x => x.Username)
131+
.ToDictionary(user => user.Key, value => value.ToList());
132+
121133
// Handle expiring credentials
122134
var jobRunTime = DateTimeOffset.UtcNow;
123-
foreach (var expiredCredential in expiredCredentials)
135+
foreach (var userCredMapping in userToExpiredCredsMapping)
124136
{
125-
if (!_contactedUsers.ContainsKey(expiredCredential.Username))
137+
var username = userCredMapping.Key;
138+
var credentialList = userCredMapping.Value;
139+
140+
// Split credentials into two lists: Expired and Expiring to aggregate messages
141+
var expiringCredentialList = credentialList
142+
.Where(x => (x.Expires - jobRunTime).TotalDays > 0)
143+
.ToList();
144+
var expiredCredentialList = credentialList
145+
.Where(x => (x.Expires - jobRunTime).TotalDays <= 0)
146+
.ToList();
147+
148+
DateTimeOffset userContactTime;
149+
if (!_contactedUsers.TryGetValue(username, out userContactTime))
126150
{
127-
await HandleExpiredCredentialEmail(expiredCredential, jobRunTime);
151+
// send expiring API keys email notification
152+
await HandleExpiredCredentialEmail(username, expiringCredentialList, jobRunTime, expired: false);
153+
154+
// send expired API keys email notification
155+
await HandleExpiredCredentialEmail(username, expiredCredentialList, jobRunTime, expired: true);
128156
}
129157
else
130158
{
131-
_logger.LogDebug("Skipping expired credential for user {Username} - already handled today.",
132-
expiredCredential.Username);
159+
_logger.LogDebug("Skipping expired credential for user {Username} - already handled at {JobRuntime}.",
160+
username, userContactTime);
133161
}
134162
}
135163
}
136164
catch (Exception ex)
137165
{
138-
_logger.LogCritical("Job run failed! {Exception}", ex);
166+
_logger.LogCritical(LogEvents.JobRunFailed, ex, "Job run failed!");
139167

140168
return false;
141169
}
@@ -149,24 +177,37 @@ public override async Task<bool> Run()
149177
return true;
150178
}
151179

152-
private async Task HandleExpiredCredentialEmail(ExpiredCredentialData expiredCredential, DateTimeOffset jobRunTime)
180+
private async Task HandleExpiredCredentialEmail(string username, List<ExpiredCredentialData> credentialList, DateTimeOffset jobRunTime, bool expired)
153181
{
154-
_logger.LogInformation("Handling expired credential for user {Username} (expires: {Expires})...", expiredCredential.Username, expiredCredential.Expires);
182+
if (credentialList == null || credentialList.Count == 0)
183+
{
184+
return;
185+
}
186+
187+
_logger.LogInformation("Handling {Expired} credential(s) for user {Username} (Keys: {Descriptions})...",
188+
expired ? "expired" : "expiring",
189+
username,
190+
string.Join(", ", credentialList.Select(x => x.Description).ToList()));
155191

156192
// Build message
157-
var mailMessage = new MailMessage(_mailFrom, expiredCredential.EmailAddress);
193+
var userEmail = credentialList.FirstOrDefault().EmailAddress;
194+
var mailMessage = new MailMessage(_mailFrom, userEmail);
158195

196+
var apiKeyExpiryMessageList = credentialList
197+
.Select(x => BuildApiKeyExpiryMessage(x.Description, x.Expires, jobRunTime))
198+
.ToList();
199+
200+
var apiKeyExpiryMessage = string.Join(Environment.NewLine, apiKeyExpiryMessageList);
159201
// Build email body
160-
var expiresInDays = expiredCredential.Expires.UtcDateTime - DateTime.UtcNow;
161-
if (expiresInDays.TotalDays <= 0)
202+
if (expired)
162203
{
163204
mailMessage.Subject = string.Format(Strings.ExpiredEmailSubject, _galleryBrand);
164-
mailMessage.Body = string.Format(Strings.ExpiredEmailBody, _galleryBrand, _galleryAccountUrl);
205+
mailMessage.Body = string.Format(Strings.ExpiredEmailBody, username, _galleryBrand, apiKeyExpiryMessage, _galleryAccountUrl);
165206
}
166207
else
167208
{
168209
mailMessage.Subject = string.Format(Strings.ExpiringEmailSubject, _galleryBrand);
169-
mailMessage.Body = string.Format(Strings.ExpiringEmailBody, _galleryBrand, _galleryAccountUrl, (int)expiresInDays.TotalDays);
210+
mailMessage.Body = string.Format(Strings.ExpiringEmailBody, username, _galleryBrand, apiKeyExpiryMessage, _galleryAccountUrl);
170211
}
171212

172213
// Send email
@@ -177,22 +218,37 @@ private async Task HandleExpiredCredentialEmail(ExpiredCredentialData expiredCre
177218
await _smtpClient.SendMailAsync(mailMessage);
178219
}
179220

180-
_logger.LogInformation("Handled expired credential for user {Username}.", expiredCredential.Username);
221+
_logger.LogInformation("Handled {Expired} credential for user {Username}.",
222+
expired ? "expired" : "expiring",
223+
username);
181224

182-
_contactedUsers.AddOrUpdate(expiredCredential.Username, jobRunTime, (s, offset) => jobRunTime);
225+
_contactedUsers.AddOrUpdate(username, jobRunTime, (s, offset) => jobRunTime);
183226
}
184227
catch (SmtpFailedRecipientException ex)
185228
{
186-
_logger.LogWarning("Failed to handle expired credential for user {Username} - recipient failed.", expiredCredential.Username, ex);
229+
var logMessage = $"Failed to handle credential for user {username} - recipient failed!";
230+
_logger.LogWarning(LogEvents.FailedToSendMail, ex, logMessage);
187231
}
188232
catch (Exception ex)
189233
{
190-
_logger.LogCritical("Failed to handle expired credential for user {Username}.", expiredCredential.Username, ex);
234+
var logMessage = $"Failed to handle credential for user {username}.";
235+
_logger.LogCritical(LogEvents.FailedToHandleExpiredCredential, ex, logMessage);
191236

192237
throw;
193238
}
194239
}
195240

241+
private static string BuildApiKeyExpiryMessage(string description, DateTimeOffset expiry, DateTimeOffset currentTime)
242+
{
243+
var expiryInDays = (expiry - currentTime).TotalDays;
244+
var message = expiryInDays < 0
245+
? string.Format(Strings.ApiKeyExpired, description)
246+
: string.Format(Strings.ApiKeyExpiring, description, (int)expiryInDays);
247+
248+
// \u2022 - Unicode for bullet point.
249+
return "\u2022 "+ message + Environment.NewLine;
250+
}
251+
196252
private SmtpClient CreateSmtpClient(SmtpUri smtpUri)
197253
{
198254
var smtpClient = new SmtpClient(smtpUri.Host, smtpUri.Port)
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using Microsoft.Extensions.Logging;
5+
6+
namespace Gallery.CredentialExpiration
7+
{
8+
public class LogEvents
9+
{
10+
public static EventId FailedToHandleExpiredCredential = new EventId(600, "Failed to handle expired credential");
11+
public static EventId FailedToSendMail = new EventId(601, "Failed to deliver email");
12+
public static EventId JobRunFailed = new EventId(650, "Job run failed");
13+
public static EventId JobInitFailed = new EventId(651, "Job initialization failed");
14+
}
15+
}

src/Gallery.CredentialExpiration/Models/ExpiredCredentialData.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,17 @@
55

66
namespace Gallery.CredentialExpiration.Models
77
{
8+
public static class Constants
9+
{
10+
public const string NonScopedApiKeyDescription = "Full access API key";
11+
}
12+
813
public class ExpiredCredentialData
914
{
1015
public string Type { get; set; }
1116
public string Username { get; set; }
1217
public string EmailAddress { get; set; }
18+
public string Description { get; set; }
1319
public DateTimeOffset Created { get; set; }
1420
public DateTimeOffset Expires { get; set; }
1521
}

src/Gallery.CredentialExpiration/MyJobArgumentNames.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,5 +8,6 @@ public class MyJobArgumentNames
88
public const string GalleryBrand = "GalleryBrand";
99
public const string GalleryAccountUrl = "GalleryAccountUrl";
1010
public const string WarnDaysBeforeExpiration = "WarnDaysBeforeExpiration";
11+
public const string AllowEmailResendAfterDays = "AllowEmailResendAfterDays";
1112
}
1213
}

src/Gallery.CredentialExpiration/Strings.Designer.cs

Lines changed: 33 additions & 11 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)