Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 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
9 changes: 9 additions & 0 deletions TODO.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# Acceptances tests:

## Active ad dates

- Create Partner
- Partner dashboard
- Partner Details (both as partner and admin)

## Actively looking
Comment thread
NielsPilgaard marked this conversation as resolved.
Outdated
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
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
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
89 changes: 89 additions & 0 deletions src/web/Jordnaer/Features/Partners/PartnerService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,20 @@ public interface IPartnerService
Task<OneOf<Success, Error<string>>> UploadPendingChangesAsync(Guid partnerId, Stream? adImageStream, string? adImageFileName, string? name, string? description, Stream? logoStream, string? logoFileName, string? partnerPageLink, string? adLink, string? adLabelColor, bool clearAdLabelColor, CancellationToken cancellationToken = default);
Task<OneOf<Success, Error<string>>> ApproveChangesAsync(Guid partnerId, CancellationToken cancellationToken = default);
Task<OneOf<Success, Error<string>>> RejectChangesAsync(Guid partnerId, CancellationToken cancellationToken = default);
Task<OneOf<Success, NotFound, Error<string>>> UpdatePartnerAsync(Guid partnerId, UpdatePartnerRequest request, CancellationToken cancellationToken = default);
}

public record UpdatePartnerRequest
{
public string? Name { get; init; }
public string? Description { get; init; }
public string? PartnerPageLink { get; init; }
public string? AdLink { get; init; }
public string? AdLabelColor { get; init; }
public bool CanHavePartnerCard { get; init; }
public bool CanHaveAd { get; init; }
public DateTime? DisplayStartUtc { get; init; }
public DateTime? DisplayEndUtc { get; init; }
}

public class PartnerService(
Expand Down Expand Up @@ -797,6 +811,81 @@ public async Task<OneOf<Success, Error<string>>> RejectChangesAsync(Guid partner
return new Error<string>("Failed to reject changes");
}
}

public async Task<OneOf<Success, NotFound, Error<string>>> UpdatePartnerAsync(Guid partnerId, UpdatePartnerRequest request, CancellationToken cancellationToken = default)
{
logger.LogFunctionBegan();

try
{
if (!currentUser.User.IsInRole(AppRoles.Admin))
{
return new Error<string>("Unauthorized: Admin role required");
}

// Validate display date range
if (request.DisplayStartUtc.HasValue && request.DisplayEndUtc.HasValue &&
request.DisplayStartUtc.Value >= request.DisplayEndUtc.Value)
{
return new Error<string>("Startdato skal være før slutdato");
}

// Validate URLs
if (!string.IsNullOrWhiteSpace(request.PartnerPageLink))
{
if (!Uri.TryCreate(request.PartnerPageLink.Trim(), UriKind.Absolute, out var uri) ||
(uri.Scheme != Uri.UriSchemeHttp && uri.Scheme != Uri.UriSchemeHttps))
{
return new Error<string>("Ugyldig partnerside link URL");
}
}

if (!string.IsNullOrWhiteSpace(request.AdLink))
{
if (!Uri.TryCreate(request.AdLink.Trim(), UriKind.Absolute, out var uri) ||
(uri.Scheme != Uri.UriSchemeHttp && uri.Scheme != Uri.UriSchemeHttps))
{
return new Error<string>("Ugyldig annonce link URL");
}
}

if (!string.IsNullOrWhiteSpace(request.AdLabelColor))
{
if (!System.Text.RegularExpressions.Regex.IsMatch(request.AdLabelColor.Trim(), "^#[0-9A-Fa-f]{6}$"))
{
return new Error<string>("Ugyldig farve. Brug hex format som #FFFFFF");
}
}

await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
var partner = await context.Partners.FirstOrDefaultAsync(s => s.Id == partnerId, cancellationToken);

if (partner is null)
{
return new NotFound();
}

partner.Name = string.IsNullOrWhiteSpace(request.Name) ? null : request.Name.Trim();
partner.Description = string.IsNullOrWhiteSpace(request.Description) ? null : request.Description.Trim();
partner.PartnerPageLink = string.IsNullOrWhiteSpace(request.PartnerPageLink) ? null : request.PartnerPageLink.Trim();
partner.AdLink = string.IsNullOrWhiteSpace(request.AdLink) ? null : request.AdLink.Trim();
partner.AdLabelColor = string.IsNullOrWhiteSpace(request.AdLabelColor) ? null : request.AdLabelColor.Trim();
partner.CanHavePartnerCard = request.CanHavePartnerCard;
partner.CanHaveAd = request.CanHaveAd;
partner.DisplayStartUtc = request.DisplayStartUtc;
partner.DisplayEndUtc = request.DisplayEndUtc;
partner.LastUpdateUtc = DateTime.UtcNow;

await context.SaveChangesAsync(cancellationToken);

return new Success();
}
catch (Exception ex)
{
logger.LogError(ex, "Failed to update partner {PartnerId}", partnerId);
return new Error<string>("Kunne ikke opdatere partner. Prøv igen senere.");
}
}
}

public class PartnerAnalyticsDto
Expand Down
13 changes: 12 additions & 1 deletion src/web/Jordnaer/Features/Partners/PartnerUserService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ public record CreatePartnerRequest
public required string Link { get; init; }
public bool CanHavePartnerCard { get; init; } = true;
public bool CanHaveAd { get; init; } = true;
public DateTime? DisplayStartUtc { get; init; }
public DateTime? DisplayEndUtc { get; init; }
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}

public record CreatePartnerResult
Expand Down Expand Up @@ -69,6 +71,13 @@ public async Task<OneOf<CreatePartnerResult, Error<string>>> CreatePartnerAccoun
return new Error<string>("En bruger med denne email findes allerede");
}

// Validate display date range
if (request.DisplayStartUtc.HasValue && request.DisplayEndUtc.HasValue &&
request.DisplayStartUtc.Value >= request.DisplayEndUtc.Value)
{
return new Error<string>("Startdato skal være før slutdato");
}

// Validate URL format
if (!Uri.TryCreate(request.Link, UriKind.Absolute, out var uri) ||
(uri.Scheme != Uri.UriSchemeHttp && uri.Scheme != Uri.UriSchemeHttps))
Expand Down Expand Up @@ -140,7 +149,9 @@ public async Task<OneOf<CreatePartnerResult, Error<string>>> CreatePartnerAccoun
PartnerPageLink = request.Link,
UserId = user.Id,
CanHavePartnerCard = request.CanHavePartnerCard,
CanHaveAd = request.CanHaveAd
CanHaveAd = request.CanHaveAd,
DisplayStartUtc = request.DisplayStartUtc,
DisplayEndUtc = request.DisplayEndUtc
};
context.Partners.Add(partner);

Expand Down
Loading
Loading