Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
6 changes: 3 additions & 3 deletions .github/workflows/website_frontend_ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,9 @@ jobs:
--logger "GitHubActions;summary.includePassedTests=true;summary.includeSkippedTests=true"
--filter Category!=SkipInCi
env:
Playwright_BaseUrl: "https://mini-moeder.dk"
Playwright_Username: ${{ secrets.Playwright_Username }}
Playwright_Password: ${{ secrets.Playwright_Password }}
Playwright__BaseUrl: "https://mini-moeder.dk"
Playwright__Username: ${{ secrets.Playwright_Username }}
Playwright__Password: ${{ secrets.Playwright_Password }}

- name: Upload Screenshots
if: always()
Expand Down
3 changes: 3 additions & 0 deletions src/shared/Jordnaer.Shared/Database/Group.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using Microsoft.EntityFrameworkCore;
using NetTopologySuite.Geometries;

namespace Jordnaer.Shared;

Expand All @@ -23,6 +24,8 @@ public class Group
[MaxLength(100, ErrorMessage = "By navn må højest være 100 karakterer langt.")]
public string? City { get; set; }

public Point? Location { get; set; }

[MinLength(2, ErrorMessage = "Gruppe navn skal være mindst 2 karakterer langt.")]
[MaxLength(128, ErrorMessage = "Gruppens navn må højest være 128 karakterer lang.")]
public required string Name { get; set; }
Expand Down
3 changes: 3 additions & 0 deletions src/shared/Jordnaer.Shared/Database/Post.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using Microsoft.EntityFrameworkCore;
using NetTopologySuite.Geometries;

namespace Jordnaer.Shared;

Expand All @@ -21,6 +22,8 @@ public class Post

public string? City { get; set; }

public Point? Location { get; set; }

[ForeignKey(nameof(UserProfile))]
public required string UserProfileId { get; set; } = null!;

Expand Down
3 changes: 3 additions & 0 deletions src/shared/Jordnaer.Shared/Database/UserProfile.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using Microsoft.EntityFrameworkCore;
using NetTopologySuite.Geometries;

namespace Jordnaer.Shared;

Expand Down Expand Up @@ -39,6 +40,8 @@ public class UserProfile
[MaxLength(100, ErrorMessage = "By må højest være 50 karakterer langt.")]
public string? City { get; set; }

public Point? Location { get; set; }

Comment thread
NielsPilgaard marked this conversation as resolved.
[MaxLength(2000, ErrorMessage = "Beskrivelse må højest være 2000 karakterer langt.")]
public string? Description { get; set; }

Expand Down
1 change: 1 addition & 0 deletions src/shared/Jordnaer.Shared/Jordnaer.Shared.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
<PackageReference Include="Microsoft.Extensions.Options.DataAnnotations" Version="9.0.8" />
<PackageReference Include="Microsoft.Extensions.Http.Resilience" Version="9.8.0" />
<PackageReference Include="NetEscapades.EnumGenerators" Version="1.0.0-beta12" />
<PackageReference Include="NetTopologySuite" Version="2.6.0" />
<PackageReference Include="Refit.HttpClientFactory" Version="8.0.0" />
<PackageReference Include="MassTransit.Abstractions" Version="8.5.2" />
</ItemGroup>
Expand Down
10 changes: 5 additions & 5 deletions src/web/Jordnaer/Components/Account/Pages/ConfirmEmail.razor
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,10 @@
<MetadataComponent Title="Bekræft email" />

<div class="account-container">
<div class="account-paper">
<h1 class="account-header">Bekræft email</h1>
<StatusMessage Message="@_statusMessage" />
</div>
<div class="account-paper">
<h1 class="account-header">Bekræft email</h1>
<StatusMessage Message="@_statusMessage" />
</div>
</div>

@code {
Expand Down Expand Up @@ -54,7 +54,7 @@

await SignInManager.SignInAsync(user, isPersistent: true);

var returnUrl = string.IsNullOrEmpty(ReturnUrl) ? "/profile" : ReturnUrl;
var returnUrl = string.IsNullOrEmpty(ReturnUrl) ? "/" : ReturnUrl;

RedirectManager.RedirectTo(returnUrl, forceLoad: true);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,84 +1,20 @@
@page "/Account/Manage/DeletePersonalData"

@inject UserManager<ApplicationUser> UserManager
@inject SignInManager<ApplicationUser> SignInManager
@inject IdentityUserAccessor UserAccessor
@inject IdentityRedirectManager RedirectManager
@inject ILogger<DeletePersonalData> Logger

@attribute [Sitemap]

<MetadataComponent Title="Slet personlige data"/>

<h2 class="manage-page-title">Slet personlige data</h2>

<StatusMessage Message="@_message" />

<div class="alert alert-warning" role="alert">
<p>
<strong>Når du sletter disse data, vil din konto blive permanent fjernet, og dette kan ikke fortrydes.</strong>
</p>
</div>

<EditForm Model="Input" FormName="delete-user" OnValidSubmit="OnValidSubmitAsync" method="post" class="account-form" style="max-width: 600px;">
<DataAnnotationsValidator />
<ValidationSummary class="validation-message" role="alert" />

@if (_requirePassword)
{
<div class="form-floating">
<InputText type="password" @bind-Value="Input.Password" class="form-control" autocomplete="current-password" aria-required="true" placeholder="Indtast venligst din adgangskode." autofocus />
<label for="password" class="form-label">Adgangskode</label>
<ValidationMessage For="() => Input.Password" class="validation-message" />
</div>
}

<button class="account-button-primary" type="submit" style="background-color: var(--danger-color);">Slet data og luk min konto</button>
</EditForm>


@code {
private AlertMessage? _message;
private ApplicationUser _user = default!;
private bool _requirePassword;

[CascadingParameter]
private HttpContext HttpContext { get; set; } = default!;

[SupplyParameterFromForm]
private InputModel Input { get; set; } = new();

protected override async Task OnInitializedAsync()
{
_user = await UserAccessor.GetRequiredUserAsync(HttpContext);
_requirePassword = await UserManager.HasPasswordAsync(_user);
}

private async Task OnValidSubmitAsync()
{
if (_requirePassword && !await UserManager.CheckPasswordAsync(_user, Input.Password))
{
_message = new AlertMessage("Forkert adgangskode.", true);
return;
}

var result = await UserManager.DeleteAsync(_user);
if (!result.Succeeded)
{
throw new InvalidOperationException("Unexpected error occurred deleting user.");
}
<MetadataComponent Title="Slet bruger" />

await SignInManager.SignOutAsync();
<h2 class="manage-page-title">Slet bruger</h2>

var userId = await UserManager.GetUserIdAsync(_user);
Logger.LogInformation("User with ID '{UserId}' deleted themselves.", userId);
<StatusMessage />

RedirectManager.RedirectToCurrentPage();
}
<div style="max-width: 600px;">
<MudText Typo="Typo.body1" Class="mb-4">
<strong>Når du sletter din bruger, vil din konto blive permanent fjernet, og dette kan ikke fortrydes.</strong>
</MudText>

private sealed class InputModel
{
[DataType(DataType.Password)]
public string Password { get; set; } = "";
}
}
<MudButton StartIcon="@Icons.Material.Filled.Warning" Size="Size.Large" Href="/InitializeDeleteUser"
Color="Color.Error" Variant="Variant.Outlined">
Slet Bruger
</MudButton>
</div>
Original file line number Diff line number Diff line change
Expand Up @@ -12,17 +12,12 @@
<StatusMessage />

<div style="max-width: 600px;">
<p>Din konto indeholder personlige data, som du har givet os. Denne side giver dig mulighed for at downloade eller slette disse data.</p>
<p>
<strong>At slette disse data vil permanent fjerne din konto, og dette kan ikke fortrydes.</strong>
</p>
<p>Din konto indeholder personlige data, som du har givet os. Denne side giver dig mulighed for at downloade disse data.</p>

<form action="Account/Manage/DownloadPersonalData" method="post" style="margin-bottom: 1rem;">
<form action="Account/Manage/DownloadPersonalData" method="post">
<AntiforgeryToken />
<button class="account-button-primary" type="submit">Download</button>
</form>

<a href="Account/Manage/DeletePersonalData" class="account-button-primary" style="background-color: var(--danger-color); display: inline-block; text-decoration: none;">Slet</a>
</div>


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,12 @@
Personlig data
</NavLink>
</li>
<li class="nav-item">
<NavLink class="nav-link" href="Account/Manage/DeletePersonalData"
style="font-family: 'Open Sans Light', sans-serif; color: #41556b; border-radius: 4px; padding: 0.75rem 1rem; min-height: 44px; display: flex; align-items: center;">
Slet bruger
</NavLink>
</li>
</ul>
</nav>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,8 @@ public static WebApplicationBuilder AddDatabase(this WebApplicationBuilder build
var connectionString = GetConnectionString(builder.Configuration);

builder.Services.AddDbContextFactory<JordnaerDbContext>(
optionsBuilder => optionsBuilder.UseAzureSql(connectionString),
optionsBuilder => optionsBuilder
.UseSqlServer(connectionString, sqlOptions => sqlOptions.UseNetTopologySuite()),
ServiceLifetime.Scoped);

builder.Services.AddHealthChecks().AddSqlServer(connectionString);
Expand Down
70 changes: 34 additions & 36 deletions src/web/Jordnaer/Features/GroupSearch/GroupSearchService.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
using Jordnaer.Database;
using Jordnaer.Features.Metrics;
using Jordnaer.Features.Search;
using Jordnaer.Features.Profile;
using Jordnaer.Shared;
using Microsoft.EntityFrameworkCore;

Expand All @@ -13,7 +13,7 @@ public interface IGroupSearchService

public class GroupSearchService(
IDbContextFactory<JordnaerDbContext> contextFactory,
IZipCodeService zipCodeService)
ILocationService locationService)
: IGroupSearchService
{
public async Task<GroupSearchResult> GetGroupsAsync(GroupSearchFilter filter,
Expand All @@ -32,60 +32,58 @@ public async Task<GroupSearchResult> GetGroupsAsync(GroupSearchFilter filter,

if (!isOrdered)
{
groups = groups.OrderBy(user => user.CreatedUtc);
groups = groups.OrderBy(group => group.CreatedUtc);
}

// TODO: Try-catch and error in return type
var totalCount = await groups.CountAsync(cancellationToken);

var groupsToSkip = filter.PageNumber == 1 ? 0 : (filter.PageNumber - 1) * filter.PageSize;
var paginatedGroups = await groups
.Skip(groupsToSkip)
.Take(filter.PageSize)
.Include(user => user.Categories)
.AsSingleQuery()
.Select(group => new GroupSlim
{
ProfilePictureUrl = group.ProfilePictureUrl,
Name = group.Name,
ShortDescription = group.ShortDescription,
Description = group.Description,
ZipCode = group.ZipCode,
City = group.City,
Categories = group.Categories.Select(category => category.Name).ToArray(),
MemberCount = group.Memberships.Count(e => e.MembershipStatus == MembershipStatus.Active),
Id = group.Id
})
.AsNoTracking()
.ToListAsync(cancellationToken);

var totalCount = await groups.AsNoTracking().CountAsync(cancellationToken);
.Skip(groupsToSkip)
.Take(filter.PageSize)
.Select(group => new GroupSlim
{
ProfilePictureUrl = group.ProfilePictureUrl,
Name = group.Name,
ShortDescription = group.ShortDescription,
Description = group.Description,
ZipCode = group.ZipCode,
City = group.City,
Categories = group.Categories.Select(category => category.Name).ToArray(),
MemberCount = group.Memberships.Count(e => e.MembershipStatus == MembershipStatus.Active),
Id = group.Id
})
.ToListAsync(cancellationToken);

return new GroupSearchResult { TotalCount = totalCount, Groups = paginatedGroups };
}

internal async Task<(IQueryable<Group> Groups, bool AppliedOrdering)>
ApplyLocationFilterAsync(IQueryable<Group> groups, GroupSearchFilter filter, CancellationToken cancellationToken = default)
internal async Task<(IQueryable<Group> Groups, bool AppliedOrdering)> ApplyLocationFilterAsync(
IQueryable<Group> groups,
GroupSearchFilter filter,
CancellationToken cancellationToken = default)
{
if (string.IsNullOrEmpty(filter.Location) || filter.WithinRadiusKilometers is null)
{
return (groups, false);
}

var (zipCodesWithinCircle, searchedZipCode) = await zipCodeService.GetZipCodesNearLocationAsync(
filter.Location,
filter.WithinRadiusKilometers.Value,
cancellationToken);
// Get location from the search location
var searchLocation = await locationService.GetLocationFromZipCodeAsync(filter.Location, cancellationToken);

if (zipCodesWithinCircle.Count is 0 || searchedZipCode is null)
if (searchLocation is null)
{
return (groups, false);
}

groups = groups
.Where(group => group.ZipCode != null &&
zipCodesWithinCircle.Contains(group.ZipCode.Value))
.OrderBy(group => Math.Abs(group.ZipCode!.Value - searchedZipCode.Value));
var location = searchLocation.Location;
var radiusMeters = filter.WithinRadiusKilometers.Value * 1000;

// Use SQL Server's built-in distance calculation with geography type
var groupsWithDistance = groups.Where(g => g.Location != null && g.Location.IsWithinDistance(location, radiusMeters))
.OrderBy(g => g.Location!.Distance(location));

return (groups, true);
return (groupsWithDistance, true);
}

private static ReadOnlySpan<KeyValuePair<string, object?>> MakeTagList(GroupSearchFilter filter)
Expand Down
Loading