Skip to content
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions TODO.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# Acceptances tests
18 changes: 18 additions & 0 deletions src/shared/Jordnaer.Shared/Database/Partner.cs
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,16 @@ public class Partner
/// </summary>
public bool CanHavePartnerCard { get; set; } = true;

/// <summary>
/// When ads/card should start showing. Null means no start restriction.
/// </summary>
public DateTime? DisplayStartUtc { get; set; }

/// <summary>
/// When ads/card should stop showing. Null means no end restriction.
/// </summary>
public DateTime? DisplayEndUtc { get; set; }

public DateTime CreatedUtc { get; set; }

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

/// <summary>
/// Returns whether the partner is within its display time window.
/// Null values mean "no restriction" for that bound.
/// </summary>
public bool IsWithinDisplayWindow(DateTime utcNow) =>
(DisplayStartUtc is null || utcNow >= DisplayStartUtc) &&
(DisplayEndUtc is null || utcNow <= DisplayEndUtc);

/// <summary>
/// Validates that the partner has at least one type of presence (partner card or ad image)
/// </summary>
Expand Down
9 changes: 8 additions & 1 deletion src/shared/Jordnaer.Shared/Validation/HexColorAttribute.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,16 @@ public override bool IsValid(object? value)
return false;
}

return HexColorRegex().IsMatch(hexColor);
return IsValidHexColor(hexColor);
}

/// <summary>
/// Checks whether the given string is a valid hex color in #RGB or #RRGGBB format.
/// Returns <c>false</c> for null, empty, or whitespace-only strings.
/// </summary>
public static bool IsValidHexColor([System.Diagnostics.CodeAnalysis.NotNullWhen(true)] string? color)
=> color is not null && HexColorRegex().IsMatch(color);

[GeneratedRegex(@"^#([0-9A-Fa-f]{3}|[0-9A-Fa-f]{6})$")]
private static partial Regex HexColorRegex();
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,10 @@ internal sealed class IdentityRedirectManager(NavigationManager navigationManage
[DoesNotReturn]
public void RedirectTo(string? uri, bool forceLoad = false)
{
uri ??= "";
if (string.IsNullOrEmpty(uri))
{
uri = "/";
}

// Prevent open redirects (including scheme-relative URLs like "//evil.com")
if (!IsSafeRelativeUrl(uri))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@
{
if (UserId is null || Code is null)
{
RedirectManager.RedirectTo("");
RedirectManager.RedirectTo("/");
}

var user = await UserManager.FindByIdAsync(UserId);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
@page "/Account/ExternalLogin"
@using System.Text
@using System.Text.Encodings.Web
@using Mediator
@using Microsoft.AspNetCore.WebUtilities
@using Jordnaer.Features.Profile
Expand Down Expand Up @@ -229,7 +228,7 @@
NavigationManager.ToAbsoluteUri("Account/ConfirmEmail").AbsoluteUri,
new Dictionary<string, object?> { ["userId"] = user.Id, ["code"] = code });

await EmailSender.SendConfirmationLinkAsync(user, Input.Email, HtmlEncoder.Default.Encode(callbackUrl));
await EmailSender.SendConfirmationLinkAsync(user, Input.Email, callbackUrl);

// If account confirmation is required, we need to show the link if we don't have a real email sender
if (UserManager.Options.SignIn.RequireConfirmedAccount)
Expand Down
5 changes: 2 additions & 3 deletions src/web/Jordnaer/Components/Account/Pages/Manage/Email.razor
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
@page "/Account/Manage/Email"
@using System.Text
@using System.Text.Encodings.Web
@using Microsoft.AspNetCore.WebUtilities

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

await EmailSender.SendConfirmationLinkAsync(_user, Input.NewEmail, HtmlEncoder.Default.Encode(callbackUrl));
await EmailSender.SendConfirmationLinkAsync(_user, Input.NewEmail, callbackUrl);

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

await EmailSender.SendConfirmationLinkAsync(_user, _email, HtmlEncoder.Default.Encode(callbackUrl));
await EmailSender.SendConfirmationLinkAsync(_user, _email, callbackUrl);

_message = new AlertMessage("Bekræftelses email er sendt. Tjek venligst din email.");
}
Expand Down
3 changes: 1 addition & 2 deletions src/web/Jordnaer/Components/Account/Pages/Register.razor
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
@page "/Account/Register"
@using System.Text
@using System.Text.Encodings.Web
@using Microsoft.AspNetCore.WebUtilities
@using Microsoft.EntityFrameworkCore
@using Jordnaer.Features.Membership
Expand Down Expand Up @@ -246,7 +245,7 @@
NavigationManager.ToAbsoluteUri("Account/ConfirmEmail").AbsoluteUri,
callbackParams);

await EmailSender.SendConfirmationLinkAsync(user, Input.Email, HtmlEncoder.Default.Encode(callbackUrl));
await EmailSender.SendConfirmationLinkAsync(user, Input.Email, callbackUrl);

if (UserManager.Options.SignIn.RequireConfirmedAccount)
{
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
@page "/Account/ResendEmailConfirmation"
@using System.Text
@using System.Text.Encodings.Web
@using Microsoft.AspNetCore.WebUtilities

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

_message = new AlertMessage("Bekræftelses-emailen er afsendt. Tjek venligst din email.");
}
Expand Down
30 changes: 3 additions & 27 deletions src/web/Jordnaer/Consumers/GroupPostCreatedConsumer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
using MassTransit;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Options;
using System.Net;
using System.Text.RegularExpressions;

namespace Jordnaer.Consumers;
Expand Down Expand Up @@ -69,7 +68,7 @@ public async Task Consume(ConsumeContext<GroupPostCreated> consumeContext)
var email = new SendEmail
{
Subject = $"Nyt opslag i {message.GroupName}",
HtmlContent = CreateNewPostEmailContent(message.AuthorDisplayName, postPreview, groupUrl),
HtmlContent = CreateNewPostEmailContent(baseUrl, message.AuthorDisplayName, postPreview, groupUrl),
Bcc = emails
};

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

private static string CreateNewPostEmailContent(string authorName, string postPreview, string groupUrl)
{
// HTML-encode to prevent XSS attacks
var encodedAuthorName = WebUtility.HtmlEncode(authorName);
var encodedPostPreview = WebUtility.HtmlEncode(postPreview);

// Convert newlines to <br/> tags for proper display after encoding
encodedPostPreview = encodedPostPreview.Replace("\r\n", "<br/>").Replace("\n", "<br/>");

var encodedGroupUrl = WebUtility.HtmlEncode(groupUrl);

return $"""
<h4>Nyt opslag i din gruppe</h4>

<p><b>{encodedAuthorName}</b> har oprettet et nyt opslag:</p>

<blockquote style="border-left: 3px solid #ccc; padding-left: 10px; color: #666;">
{encodedPostPreview}
</blockquote>

<p><a href="{encodedGroupUrl}">Klik her for at se opslaget</a></p>

{EmailConstants.Signature}
""";
}
private static string CreateNewPostEmailContent(string baseUrl, string authorName, string postPreview, string groupUrl) =>
EmailContentBuilder.GroupPostNotification(baseUrl, authorName, postPreview, groupUrl);

[GeneratedRegex("<.*?>")]
private static partial Regex HtmlTagsRegex();
Expand Down
3 changes: 3 additions & 0 deletions src/web/Jordnaer/Features/Ad/AdProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,12 @@ public async Task<OneOf<List<AdData>, Error<string>>> GetAdsAsync(int count, Can
try
{
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
var utcNow = DateTime.UtcNow;
var partnerAds = await context.Partners
.AsNoTracking()
.Where(p => p.CanHaveAd && p.AdImageUrl != null && p.AdImageUrl != "")
.Where(p => (p.DisplayStartUtc == null || utcNow >= p.DisplayStartUtc) &&
(p.DisplayEndUtc == null || utcNow <= p.DisplayEndUtc))
.Select(p => new AdData
{
Title = p.Name ?? "Partner",
Expand Down
21 changes: 6 additions & 15 deletions src/web/Jordnaer/Features/Chat/ChatNotificationService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ internal IEnumerable<SendEmail> CreateEmails(StartChat startChat, Dictionary<str
{
To = [recipientsEmailAddress],
Subject = $"Ny besked fra {initiator.DisplayName}",
HtmlContent = CreateNewChatEmailMessage(user.DisplayName, initiator.DisplayName, GetChatLink(startChat.Id))
HtmlContent = CreateNewChatEmailMessage(options.Value.BaseUrl, user.DisplayName, initiator.DisplayName, GetChatLink(startChat.Id))
};
}
}
Expand Down Expand Up @@ -134,7 +134,7 @@ public async Task NotifyRecipientsOfNewMessage(SendMessage message, Cancellation
{
To = [recipientEmailAddress],
Subject = $"Ny besked fra {sender.DisplayName}",
HtmlContent = CreateNewChatEmailMessage(recipientEmailAddress.DisplayName, sender.DisplayName, chatLink)
HtmlContent = CreateNewChatEmailMessage(options.Value.BaseUrl, recipientEmailAddress.DisplayName, sender.DisplayName, chatLink)
});
}

Expand All @@ -143,17 +143,8 @@ public async Task NotifyRecipientsOfNewMessage(SendMessage message, Cancellation
logger.LogInformation("Sent {Count} emails for new chat message.", emailsToSend.Count);
}

private static string CreateNewChatEmailMessage(string recipientDisplayName,
private static string CreateNewChatEmailMessage(string baseUrl, string recipientDisplayName,
string messageSenderDisplayName,
string link) => $"""
{EmailConstants.Greeting(recipientDisplayName)}

<p>Du har fået en ny besked fra <b>{messageSenderDisplayName}</b></p>

<p>Hvis du vil gå direkte til beskeden, kan du klikke på linket nedenfor:</p>

<p><a href="{link}">Læs besked</a></p>

{EmailConstants.Signature}
""";
}
string link) =>
EmailContentBuilder.ChatNotification(baseUrl, recipientDisplayName, messageSenderDisplayName, link);
}
24 changes: 4 additions & 20 deletions src/web/Jordnaer/Features/DeleteUser/DeleteUserService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ public async Task<bool> InitiateDeleteUserAsync(string userId, CancellationToken

var deletionLink = $"{baseUri}/delete-user/{token}";

var message = CreateDeleteUserEmailMessage(deletionLink);
var message = CreateDeleteUserEmailMessage(baseUri, deletionLink);

var email = new SendEmail
{
Expand Down Expand Up @@ -168,24 +168,8 @@ public async Task<bool> VerifyTokenAsync(string userId,
return false;
}

private static string CreateDeleteUserEmailMessage(string deletionLink) =>
$"""

<p>Hej,</p>

<p>Du har anmodet om at slette din bruger hos Mini Møder. Hvis du fortsætter, vil alle dine data blive permanent slettet og kan ikke genoprettes.</p>

<p>Hvis du er sikker på, at du vil slette din bruger, skal du klikke på linket nedenfor:</p>

<p><a href="{deletionLink}">Bekræft sletning af bruger</a></p>

<p>Hvis du ikke anmodede om at slette din bruger, kan du ignorere denne e-mail.</p>

<p>Venlig hilsen,</p>

<p>Mini Møder teamet</p>

""";
private static string CreateDeleteUserEmailMessage(string baseUrl, string deletionLink) =>
EmailContentBuilder.DeleteUser(baseUrl, deletionLink);
}

public record UserDeleted(string Id);
public record UserDeleted(string Id);
9 changes: 3 additions & 6 deletions src/web/Jordnaer/Features/Email/EmailConstants.cs
Original file line number Diff line number Diff line change
@@ -1,15 +1,12 @@
using System.Net;

namespace Jordnaer.Features.Email;

public static class EmailConstants
{
public static readonly EmailRecipient ContactEmail = new() { Email = "kontakt@mini-moeder.dk", DisplayName = "Kontakt @ Mini Møder" };

public static readonly string Signature = """
<p>Venlig hilsen,<br />
<p>Mini Møder Teamet</p>
""";

internal static string Greeting(string? userName) => userName is null
? "<h4>Hej,</h4>"
: $"<h4>Hej {userName},</h4>";
: $"<h4>Hej {WebUtility.HtmlEncode(userName)},</h4>";
}
Loading
Loading