Skip to content

Commit 0e06755

Browse files
Merge pull request #499 from NielsPilgaard/feature/ad-expiration
Feature/ad expiration
2 parents 45ae00b + 43b4080 commit 0e06755

37 files changed

Lines changed: 2920 additions & 758 deletions

TODO.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
# Acceptance tests

src/shared/Jordnaer.Shared/Database/Partner.cs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,16 @@ public class Partner
125125
/// </summary>
126126
public bool CanHavePartnerCard { get; set; } = true;
127127

128+
/// <summary>
129+
/// When ads/card should start showing. Null means no start restriction.
130+
/// </summary>
131+
public DateTime? DisplayStartUtc { get; set; }
132+
133+
/// <summary>
134+
/// When ads/card should stop showing. Null means no end restriction.
135+
/// </summary>
136+
public DateTime? DisplayEndUtc { get; set; }
137+
128138
public DateTime CreatedUtc { get; set; }
129139

130140
public List<PartnerAnalytics> Analytics { get; set; } = [];
@@ -146,6 +156,14 @@ public class Partner
146156
/// </summary>
147157
public bool HasAdImage => CanHaveAd && !string.IsNullOrWhiteSpace(AdImageUrl);
148158

159+
/// <summary>
160+
/// Returns whether the partner is within its display time window.
161+
/// Null values mean "no restriction" for that bound.
162+
/// </summary>
163+
public bool IsWithinDisplayWindow(DateTime utcNow) =>
164+
(DisplayStartUtc is null || utcNow >= DisplayStartUtc) &&
165+
(DisplayEndUtc is null || utcNow <= DisplayEndUtc);
166+
149167
/// <summary>
150168
/// Validates that the partner has at least one type of presence (partner card or ad image)
151169
/// </summary>

src/shared/Jordnaer.Shared/Validation/HexColorAttribute.cs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,16 @@ public override bool IsValid(object? value)
2424
return false;
2525
}
2626

27-
return HexColorRegex().IsMatch(hexColor);
27+
return IsValidHexColor(hexColor);
2828
}
2929

30+
/// <summary>
31+
/// Checks whether the given string is a valid hex color in #RGB or #RRGGBB format.
32+
/// Returns <c>false</c> for null, empty, or whitespace-only strings.
33+
/// </summary>
34+
public static bool IsValidHexColor([System.Diagnostics.CodeAnalysis.NotNullWhen(true)] string? color)
35+
=> color is not null && HexColorRegex().IsMatch(color);
36+
3037
[GeneratedRegex(@"^#([0-9A-Fa-f]{3}|[0-9A-Fa-f]{6})$")]
3138
private static partial Regex HexColorRegex();
3239
}

src/web/Jordnaer/Components/Account/IdentityRedirectManager.cs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,10 @@ internal sealed class IdentityRedirectManager(NavigationManager navigationManage
1919
[DoesNotReturn]
2020
public void RedirectTo(string? uri, bool forceLoad = false)
2121
{
22-
uri ??= "";
22+
if (string.IsNullOrEmpty(uri))
23+
{
24+
uri = "/";
25+
}
2326

2427
// Prevent open redirects (including scheme-relative URLs like "//evil.com")
2528
if (!IsSafeRelativeUrl(uri))

src/web/Jordnaer/Components/Account/Pages/ConfirmEmail.razor

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@
4343
{
4444
if (UserId is null || Code is null)
4545
{
46-
RedirectManager.RedirectTo("");
46+
RedirectManager.RedirectTo("/");
4747
}
4848

4949
var user = await UserManager.FindByIdAsync(UserId);

src/web/Jordnaer/Components/Account/Pages/ExternalLogin.razor

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
@page "/Account/ExternalLogin"
22
@using System.Text
3-
@using System.Text.Encodings.Web
43
@using Mediator
54
@using Microsoft.AspNetCore.WebUtilities
65
@using Jordnaer.Features.Profile
@@ -229,7 +228,7 @@
229228
NavigationManager.ToAbsoluteUri("Account/ConfirmEmail").AbsoluteUri,
230229
new Dictionary<string, object?> { ["userId"] = user.Id, ["code"] = code });
231230

232-
await EmailSender.SendConfirmationLinkAsync(user, Input.Email, HtmlEncoder.Default.Encode(callbackUrl));
231+
await EmailSender.SendConfirmationLinkAsync(user, Input.Email, callbackUrl);
233232

234233
// If account confirmation is required, we need to show the link if we don't have a real email sender
235234
if (UserManager.Options.SignIn.RequireConfirmedAccount)

src/web/Jordnaer/Components/Account/Pages/Manage/Email.razor

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
@page "/Account/Manage/Email"
22
@using System.Text
3-
@using System.Text.Encodings.Web
43
@using Microsoft.AspNetCore.WebUtilities
54

65
@inject UserManager<ApplicationUser> UserManager
@@ -90,7 +89,7 @@
9089
NavigationManager.ToAbsoluteUri("Account/ConfirmEmailChange").AbsoluteUri,
9190
new Dictionary<string, object?> { ["userId"] = userId, ["email"] = Input.NewEmail, ["code"] = code });
9291

93-
await EmailSender.SendConfirmationLinkAsync(_user, Input.NewEmail, HtmlEncoder.Default.Encode(callbackUrl));
92+
await EmailSender.SendConfirmationLinkAsync(_user, Input.NewEmail, callbackUrl);
9493

9594
_message = new AlertMessage("Bekræftelses email er sendt. Tjek venligst din email.");
9695
}
@@ -109,7 +108,7 @@
109108
NavigationManager.ToAbsoluteUri("Account/ConfirmEmail").AbsoluteUri,
110109
new Dictionary<string, object?> { ["userId"] = userId, ["code"] = code });
111110

112-
await EmailSender.SendConfirmationLinkAsync(_user, _email, HtmlEncoder.Default.Encode(callbackUrl));
111+
await EmailSender.SendConfirmationLinkAsync(_user, _email, callbackUrl);
113112

114113
_message = new AlertMessage("Bekræftelses email er sendt. Tjek venligst din email.");
115114
}

src/web/Jordnaer/Components/Account/Pages/Register.razor

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
@page "/Account/Register"
22
@using System.Text
3-
@using System.Text.Encodings.Web
43
@using Microsoft.AspNetCore.WebUtilities
54
@using Microsoft.EntityFrameworkCore
65
@using Jordnaer.Features.Membership
@@ -246,7 +245,7 @@
246245
NavigationManager.ToAbsoluteUri("Account/ConfirmEmail").AbsoluteUri,
247246
callbackParams);
248247

249-
await EmailSender.SendConfirmationLinkAsync(user, Input.Email, HtmlEncoder.Default.Encode(callbackUrl));
248+
await EmailSender.SendConfirmationLinkAsync(user, Input.Email, callbackUrl);
250249

251250
if (UserManager.Options.SignIn.RequireConfirmedAccount)
252251
{

src/web/Jordnaer/Components/Account/Pages/ResendEmailConfirmation.razor

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
@page "/Account/ResendEmailConfirmation"
22
@using System.Text
3-
@using System.Text.Encodings.Web
43
@using Microsoft.AspNetCore.WebUtilities
54

65
@inject UserManager<ApplicationUser> UserManager
@@ -50,7 +49,7 @@
5049
var callbackUrl = NavigationManager.GetUriWithQueryParameters(
5150
NavigationManager.ToAbsoluteUri("Account/ConfirmEmail").AbsoluteUri,
5251
new Dictionary<string, object?> { ["userId"] = userId, ["code"] = code });
53-
await EmailSender.SendConfirmationLinkAsync(user, Input.Email, HtmlEncoder.Default.Encode(callbackUrl));
52+
await EmailSender.SendConfirmationLinkAsync(user, Input.Email, callbackUrl);
5453

5554
_message = new AlertMessage("Bekræftelses-emailen er afsendt. Tjek venligst din email.");
5655
}

src/web/Jordnaer/Consumers/GroupPostCreatedConsumer.cs

Lines changed: 3 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66
using MassTransit;
77
using Microsoft.EntityFrameworkCore;
88
using Microsoft.Extensions.Options;
9-
using System.Net;
109
using System.Text.RegularExpressions;
1110

1211
namespace Jordnaer.Consumers;
@@ -69,7 +68,7 @@ public async Task Consume(ConsumeContext<GroupPostCreated> consumeContext)
6968
var email = new SendEmail
7069
{
7170
Subject = $"Nyt opslag i {message.GroupName}",
72-
HtmlContent = CreateNewPostEmailContent(message.AuthorDisplayName, postPreview, groupUrl),
71+
HtmlContent = CreateNewPostEmailContent(baseUrl, message.AuthorDisplayName, postPreview, groupUrl),
7372
Bcc = emails
7473
};
7574

@@ -96,31 +95,8 @@ private static string GetPostPreview(string text)
9695
: plainText.Substring(0, 200) + "...";
9796
}
9897

99-
private static string CreateNewPostEmailContent(string authorName, string postPreview, string groupUrl)
100-
{
101-
// HTML-encode to prevent XSS attacks
102-
var encodedAuthorName = WebUtility.HtmlEncode(authorName);
103-
var encodedPostPreview = WebUtility.HtmlEncode(postPreview);
104-
105-
// Convert newlines to <br/> tags for proper display after encoding
106-
encodedPostPreview = encodedPostPreview.Replace("\r\n", "<br/>").Replace("\n", "<br/>");
107-
108-
var encodedGroupUrl = WebUtility.HtmlEncode(groupUrl);
109-
110-
return $"""
111-
<h4>Nyt opslag i din gruppe</h4>
112-
113-
<p><b>{encodedAuthorName}</b> har oprettet et nyt opslag:</p>
114-
115-
<blockquote style="border-left: 3px solid #ccc; padding-left: 10px; color: #666;">
116-
{encodedPostPreview}
117-
</blockquote>
118-
119-
<p><a href="{encodedGroupUrl}">Klik her for at se opslaget</a></p>
120-
121-
{EmailConstants.Signature}
122-
""";
123-
}
98+
private static string CreateNewPostEmailContent(string baseUrl, string authorName, string postPreview, string groupUrl) =>
99+
EmailContentBuilder.GroupPostNotification(baseUrl, authorName, postPreview, groupUrl);
124100

125101
[GeneratedRegex("<.*?>")]
126102
private static partial Regex HtmlTagsRegex();

0 commit comments

Comments
 (0)