Eerste commit
This commit is contained in:
@@ -0,0 +1,667 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using MijnBesluit.Application.Abstractions;
|
||||
using MijnBesluit.Domain.Entities;
|
||||
using MijnBesluit.Domain.Enums;
|
||||
using MijnBesluit.Infrastructure.Persistence;
|
||||
using MijnBesluit.Application.Models;
|
||||
|
||||
namespace MijnBesluit.Infrastructure.Repositories;
|
||||
|
||||
/// <summary>Implementaties op EF Core. Eén context per bewerking, samen opgeslagen in één transactie.</summary>
|
||||
public class EfUnitOfWork : IUnitOfWork
|
||||
{
|
||||
private readonly MijnBesluitDbContext _db;
|
||||
public EfUnitOfWork(MijnBesluitDbContext db) => _db = db;
|
||||
public Task<int> SaveChangesAsync(CancellationToken ct = default) => _db.SaveChangesAsync(ct);
|
||||
}
|
||||
|
||||
public class UserRepository : IUserRepository
|
||||
{
|
||||
private readonly MijnBesluitDbContext _db;
|
||||
public UserRepository(MijnBesluitDbContext db) => _db = db;
|
||||
|
||||
// De persoon hoort er altijd bij: een account zonder persoon bestaat niet, en zonder
|
||||
// Include is user.Person leeg. Dat viel op doordat er bovenaan het scherm een e-mailadres
|
||||
// stond in plaats van een naam - de terugval als de naam niet bekend is.
|
||||
public Task<User?> FindByIdAsync(long id, CancellationToken ct = default) =>
|
||||
_db.Users.Include(u => u.Person).FirstOrDefaultAsync(u => u.Id == id, ct);
|
||||
|
||||
public Task<User?> FindByEmailAsync(string email, CancellationToken ct = default) =>
|
||||
_db.Users.Include(u => u.Person).FirstOrDefaultAsync(u => u.Email == email, ct);
|
||||
|
||||
public Task<User?> FindByPersonIdAsync(long personId, CancellationToken ct = default) =>
|
||||
_db.Users.Include(u => u.Person).FirstOrDefaultAsync(u => u.PersonId == personId, ct);
|
||||
|
||||
public Task<bool> EmailExistsAsync(string email, CancellationToken ct = default) =>
|
||||
_db.Users.AnyAsync(u => u.Email == email, ct);
|
||||
|
||||
public async Task<IReadOnlyList<User>> ListBeheerdersAsync(CancellationToken ct = default) =>
|
||||
await _db.Users.Where(u => u.IsBeheerder).OrderBy(u => u.Id).ToListAsync(ct);
|
||||
|
||||
public void Add(User user) => _db.Users.Add(user);
|
||||
public void Remove(User user) => _db.Users.Remove(user);
|
||||
}
|
||||
|
||||
/// <summary>Opslag van de eenmalige codes die per e-mail worden verstuurd.</summary>
|
||||
public class LoginCodeRepository : ILoginCodeRepository
|
||||
{
|
||||
private readonly MijnBesluitDbContext _db;
|
||||
public LoginCodeRepository(MijnBesluitDbContext db) => _db = db;
|
||||
|
||||
public Task<LoginCode?> FindOpenAsync(string email, LoginCodePurpose purpose, DateTime nowUtc,
|
||||
CancellationToken ct = default) =>
|
||||
_db.LoginCodes
|
||||
.Where(c => c.Email == email && c.Purpose == purpose && c.ConsumedAt == null && c.ExpiresAt > nowUtc)
|
||||
.OrderByDescending(c => c.CreatedAt)
|
||||
.FirstOrDefaultAsync(ct);
|
||||
|
||||
public async Task<IReadOnlyList<LoginCode>> ListOpenAsync(string email, LoginCodePurpose purpose, DateTime nowUtc,
|
||||
CancellationToken ct = default) =>
|
||||
await _db.LoginCodes
|
||||
.Where(c => c.Email == email && c.Purpose == purpose && c.ConsumedAt == null && c.ExpiresAt > nowUtc)
|
||||
.ToListAsync(ct);
|
||||
|
||||
public Task<int> CountSinceAsync(string email, DateTime sinceUtc, CancellationToken ct = default) =>
|
||||
_db.LoginCodes.CountAsync(
|
||||
c => c.Email == email && c.CreatedAt >= sinceUtc && c.SignedInAt == null, ct);
|
||||
|
||||
public async Task<int> RemoveExpiredAsync(DateTime olderThanUtc, CancellationToken ct = default)
|
||||
{
|
||||
var oud = await _db.LoginCodes.Where(c => c.ExpiresAt < olderThanUtc).ToListAsync(ct);
|
||||
_db.LoginCodes.RemoveRange(oud);
|
||||
return oud.Count;
|
||||
}
|
||||
|
||||
public async Task<int> RemoveForEmailAsync(string email, CancellationToken ct = default)
|
||||
{
|
||||
var codes = await _db.LoginCodes.Where(c => c.Email == email).ToListAsync(ct);
|
||||
_db.LoginCodes.RemoveRange(codes);
|
||||
return codes.Count;
|
||||
}
|
||||
|
||||
public void Add(LoginCode code) => _db.LoginCodes.Add(code);
|
||||
}
|
||||
|
||||
public class PersonRepository : IPersonRepository
|
||||
{
|
||||
private readonly MijnBesluitDbContext _db;
|
||||
public PersonRepository(MijnBesluitDbContext db) => _db = db;
|
||||
|
||||
public Task<Person?> FindByIdAsync(long id, CancellationToken ct = default) =>
|
||||
_db.Persons.FirstOrDefaultAsync(p => p.Id == id, ct);
|
||||
|
||||
// Geclaimde profielen eerst, en daarna op nummer. Er kunnen er meerdere zijn met hetzelfde
|
||||
// adres (zie docs/Identiteit.md); wie er één vraagt hoort in elk geval altijd dezelfde terug
|
||||
// te krijgen, en het profiel van de persoon zelf weegt zwaarder dan andermans aantekening.
|
||||
public Task<Person?> FindByEmailAsync(string email, CancellationToken ct = default) =>
|
||||
_db.Persons.Where(p => p.Email == email)
|
||||
.OrderByDescending(p => p.ClaimedByUserId != null).ThenBy(p => p.Id)
|
||||
.FirstOrDefaultAsync(ct);
|
||||
|
||||
public async Task<IReadOnlyList<Person>> ListByEmailAsync(string email, CancellationToken ct = default) =>
|
||||
await _db.Persons.Where(p => p.Email == email)
|
||||
.OrderByDescending(p => p.ClaimedByUserId != null).ThenBy(p => p.Id)
|
||||
.ToListAsync(ct);
|
||||
|
||||
public async Task<IReadOnlyList<Person>> ListAantekeningenOverAdresAsync(string email, long gebruikerId,
|
||||
CancellationToken ct = default) =>
|
||||
await _db.Persons
|
||||
.Where(p => p.Email == email
|
||||
&& p.ClaimedByUserId == null // nog van niemand zelf
|
||||
&& p.MergedIntoPersonId == null // niet al opgegaan in een ander
|
||||
&& p.ManagedByUserId != null // iemand beheert hem
|
||||
&& p.ManagedByUserId != gebruikerId // en dat ben ik niet zelf
|
||||
&& (p.LinkDeclinedByUserId == null || p.LinkDeclinedByUserId != gebruikerId))
|
||||
.OrderBy(p => p.CreatedAt)
|
||||
.ToListAsync(ct);
|
||||
|
||||
public async Task<IReadOnlyList<Person>> ListByIdsAsync(IEnumerable<long> ids, CancellationToken ct = default)
|
||||
{
|
||||
var list = ids.Distinct().ToList();
|
||||
return await _db.Persons.Where(p => list.Contains(p.Id)).ToListAsync(ct);
|
||||
}
|
||||
|
||||
public async Task<Person?> FindByObjectionTokenAsync(byte[] tokenHash, CancellationToken ct = default) =>
|
||||
await _db.Persons.FirstOrDefaultAsync(p => p.ObjectionTokenHash == tokenHash, ct);
|
||||
|
||||
public void Add(Person person) => _db.Persons.Add(person);
|
||||
public void Remove(Person person) => _db.Persons.Remove(person);
|
||||
}
|
||||
|
||||
public class FamilyRepository : IFamilyRepository
|
||||
{
|
||||
private readonly MijnBesluitDbContext _db;
|
||||
public FamilyRepository(MijnBesluitDbContext db) => _db = db;
|
||||
|
||||
public Task<Family?> FindByIdAsync(long id, CancellationToken ct = default) =>
|
||||
_db.Families.FirstOrDefaultAsync(f => f.Id == id, ct);
|
||||
|
||||
public async Task<IReadOnlyList<Family>> ListOwnedByPersonAsync(long personId, CancellationToken ct = default) =>
|
||||
await _db.Families.Where(f => f.OwnerPersonId == personId && f.IsActive).ToListAsync(ct);
|
||||
|
||||
public async Task<IReadOnlyList<FamilyMember>> ListMembersAsync(long familyId, CancellationToken ct = default) =>
|
||||
await _db.FamilyMembers.Where(m => m.FamilyId == familyId).OrderBy(m => m.Relation).ToListAsync(ct);
|
||||
|
||||
public Task<FamilyMember?> FindMemberAsync(long familyId, long personId, CancellationToken ct = default) =>
|
||||
_db.FamilyMembers.FirstOrDefaultAsync(m => m.FamilyId == familyId && m.PersonId == personId, ct);
|
||||
|
||||
public async Task<IReadOnlyList<Family>> ListFamiliesContainingAsync(long personId, CancellationToken ct = default) =>
|
||||
await _db.FamilyMembers.Where(m => m.PersonId == personId)
|
||||
.Join(_db.Families, m => m.FamilyId, f => f.Id, (m, f) => f)
|
||||
.Where(f => f.IsActive)
|
||||
.ToListAsync(ct);
|
||||
|
||||
public async Task<IReadOnlyList<long>> ListCircleAsync(long personId, CancellationToken ct = default)
|
||||
{
|
||||
// De families waar hij lid van is óf eigenaar van. Dat laatste erbij omdat een eigenaar
|
||||
// in theorie zijn eigen lidmaatschap kan hebben weggehaald.
|
||||
var families = _db.FamilyMembers.Where(m => m.PersonId == personId).Select(m => m.FamilyId)
|
||||
.Union(_db.Families.Where(f => f.OwnerPersonId == personId).Select(f => f.Id));
|
||||
|
||||
var kring = await _db.FamilyMembers
|
||||
.Where(m => families.Contains(m.FamilyId))
|
||||
.Select(m => m.PersonId)
|
||||
.Distinct()
|
||||
.ToListAsync(ct);
|
||||
|
||||
if (!kring.Contains(personId)) kring.Add(personId);
|
||||
return kring;
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<FamilyMember>> ListMembershipsOfPersonAsync(long personId, CancellationToken ct = default) =>
|
||||
await _db.FamilyMembers.Where(m => m.PersonId == personId).ToListAsync(ct);
|
||||
|
||||
public async Task<IReadOnlyList<Family>> ListOwnedAsync(long personId, CancellationToken ct = default) =>
|
||||
await _db.Families.Where(f => f.OwnerPersonId == personId).ToListAsync(ct);
|
||||
|
||||
public void Add(Family family) => _db.Families.Add(family);
|
||||
public void Remove(Family family) => _db.Families.Remove(family);
|
||||
public void Add(FamilyMember member) => _db.FamilyMembers.Add(member);
|
||||
public void Remove(FamilyMember member) => _db.FamilyMembers.Remove(member);
|
||||
}
|
||||
|
||||
public class InvitationRepository : IInvitationRepository
|
||||
{
|
||||
private readonly MijnBesluitDbContext _db;
|
||||
public InvitationRepository(MijnBesluitDbContext db) => _db = db;
|
||||
|
||||
public Task<Invitation?> FindByIdAsync(long id, CancellationToken ct = default) =>
|
||||
_db.Invitations.FirstOrDefaultAsync(i => i.Id == id, ct);
|
||||
|
||||
public Task<Invitation?> FindByTokenHashAsync(byte[] tokenHash, CancellationToken ct = default) =>
|
||||
_db.Invitations.FirstOrDefaultAsync(i => i.TokenHash == tokenHash, ct);
|
||||
|
||||
public async Task<IReadOnlyList<Invitation>> ListOpenForPersonAsync(long personId, CancellationToken ct = default) =>
|
||||
await _db.Invitations.Where(i => i.PersonId == personId &&
|
||||
(i.Status == InvitationStatus.Verzonden || i.Status == InvitationStatus.Concept))
|
||||
.ToListAsync(ct);
|
||||
|
||||
public Task<bool> HasOpenInvitationAsync(long personId, DateTime nowUtc, CancellationToken ct = default) =>
|
||||
_db.Invitations.AnyAsync(i => i.PersonId == personId && i.ExpiresAt > nowUtc &&
|
||||
(i.Status == InvitationStatus.Verzonden || i.Status == InvitationStatus.Concept), ct);
|
||||
|
||||
public async Task<IReadOnlyList<Invitation>> ListLatestForPeopleAsync(IEnumerable<long> personIds,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
// Bewust een List en geen array. Bij een array kiest een nieuwere C#-compiler
|
||||
// MemoryExtensions.Contains (de span-versie) in plaats van Enumerable.Contains, en die
|
||||
// kan Entity Framework niet vertalen: dat geeft pas op de server een ArgumentException
|
||||
// over ReadOnlySpan. Met een List is het de instantiemethode en is er niets te kiezen.
|
||||
var ids = personIds.Distinct().ToList();
|
||||
if (ids.Count == 0) return Array.Empty<Invitation>();
|
||||
|
||||
var alle = await _db.Invitations.Where(i => ids.Contains(i.PersonId)).ToListAsync(ct);
|
||||
|
||||
// Groeperen doen we hier en niet in SQL: MariaDB kan geen "laatste rij per groep" zonder
|
||||
// vensterfuncties, en het gaat om een familie, niet om een miljoen rijen.
|
||||
return alle.GroupBy(i => i.PersonId)
|
||||
.Select(g => g.OrderByDescending(i => i.SentAt ?? i.CreatedAt).First())
|
||||
.ToList();
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<Invitation>> ListAllForPersonAsync(long personId, CancellationToken ct = default) =>
|
||||
await _db.Invitations.Where(i => i.PersonId == personId).ToListAsync(ct);
|
||||
|
||||
public async Task<IReadOnlyList<Invitation>> ListSentByUserAsync(long userId, CancellationToken ct = default) =>
|
||||
await _db.Invitations.Where(i => i.CreatedByUserId == userId).ToListAsync(ct);
|
||||
|
||||
public void Add(Invitation invitation) => _db.Invitations.Add(invitation);
|
||||
public void Remove(Invitation invitation) => _db.Invitations.Remove(invitation);
|
||||
}
|
||||
|
||||
public class VaultRepository : IVaultRepository
|
||||
{
|
||||
private readonly MijnBesluitDbContext _db;
|
||||
public VaultRepository(MijnBesluitDbContext db) => _db = db;
|
||||
|
||||
public Task<VaultItem?> FindByIdAsync(long id, CancellationToken ct = default) =>
|
||||
_db.VaultItems.FirstOrDefaultAsync(v => v.Id == id, ct);
|
||||
|
||||
public async Task<IReadOnlyList<VaultItem>> ListForPersonAsync(long personId, VaultCategory? category = null,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
var q = _db.VaultItems.Where(v => v.OwnerPersonId == personId && v.DeletedAt == null);
|
||||
if (category is { } c) q = q.Where(v => v.Category == c);
|
||||
return await q.OrderBy(v => v.Category).ThenBy(v => v.Title).ToListAsync(ct);
|
||||
}
|
||||
|
||||
public Task<VaultFile?> FindFileAsync(long id, CancellationToken ct = default) =>
|
||||
_db.VaultFiles.FirstOrDefaultAsync(f => f.Id == id, ct);
|
||||
|
||||
public async Task<IReadOnlyList<VaultFile>> ListFilesForItemAsync(long vaultItemId, CancellationToken ct = default) =>
|
||||
await _db.VaultFiles
|
||||
.Where(f => f.VaultItemId == vaultItemId && f.DeletedAt == null)
|
||||
.OrderBy(f => f.CreatedAt)
|
||||
.ToListAsync(ct);
|
||||
|
||||
public async Task<IReadOnlyList<VaultFile>> ListFilesForPersonAsync(long subjectPersonId, CancellationToken ct = default) =>
|
||||
await _db.VaultFiles
|
||||
.Where(f => f.SubjectPersonId == subjectPersonId && f.DeletedAt == null)
|
||||
.OrderBy(f => f.CreatedAt)
|
||||
.ToListAsync(ct);
|
||||
|
||||
public async Task<IReadOnlyList<VaultFile>> ListFilesForMessageAsync(long messageId, CancellationToken ct = default) =>
|
||||
await _db.VaultFiles
|
||||
.Where(f => f.MessageId == messageId && f.DeletedAt == null)
|
||||
.OrderBy(f => f.CreatedAt)
|
||||
.ToListAsync(ct);
|
||||
|
||||
public async Task<IReadOnlyList<VaultFile>> ListProfilePhotosAsync(IEnumerable<long> personIds,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
// Een List en niet een array: op een array kiest de compiler MemoryExtensions.Contains,
|
||||
// en dat kan EF niet naar SQL vertalen. Zie de opmerking bij ListByIdsAsync.
|
||||
var ids = personIds.Distinct().ToList();
|
||||
if (ids.Count == 0) return Array.Empty<VaultFile>();
|
||||
|
||||
return await _db.VaultFiles
|
||||
.Where(f => f.SubjectPersonId != null && ids.Contains(f.SubjectPersonId!.Value)
|
||||
&& f.IsProfilePhoto && f.DeletedAt == null)
|
||||
.ToListAsync(ct);
|
||||
}
|
||||
|
||||
public Task<ContentGrant?> FindGrantAsync(string resourceType, long resourceId, long userId,
|
||||
CancellationToken ct = default) =>
|
||||
_db.ContentGrants.FirstOrDefaultAsync(g => g.ResourceType == resourceType &&
|
||||
g.ResourceId == resourceId && g.UserId == userId, ct);
|
||||
|
||||
public async Task<IReadOnlyList<VaultItem>> ListSharedWithAsync(long userId, CancellationToken ct = default)
|
||||
{
|
||||
var itemIds = _db.ContentGrants
|
||||
.Where(g => g.ResourceType == nameof(VaultItem) && g.UserId == userId && g.RevokedAt == null)
|
||||
.Select(g => g.ResourceId);
|
||||
|
||||
return await _db.VaultItems
|
||||
.Where(v => itemIds.Contains(v.Id) && v.DeletedAt == null)
|
||||
.OrderBy(v => v.Category).ThenBy(v => v.Title)
|
||||
.ToListAsync(ct);
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<VaultFile>> ListFilesOwnedByAsync(long ownerPersonId, CancellationToken ct = default) =>
|
||||
await _db.VaultFiles.Where(f => f.OwnerPersonId == ownerPersonId).ToListAsync(ct);
|
||||
|
||||
public void Add(VaultItem item) => _db.VaultItems.Add(item);
|
||||
public void Add(VaultFile file) => _db.VaultFiles.Add(file);
|
||||
public void Add(ContentGrant grant) => _db.ContentGrants.Add(grant);
|
||||
public void Remove(VaultItem item) => _db.VaultItems.Remove(item);
|
||||
public void Remove(VaultFile file) => _db.VaultFiles.Remove(file);
|
||||
|
||||
public async Task RemoveGrantsForUserAsync(long userId, CancellationToken ct = default)
|
||||
{
|
||||
var verleend = await _db.ContentGrants.Where(g => g.UserId == userId).ToListAsync(ct);
|
||||
_db.ContentGrants.RemoveRange(verleend);
|
||||
}
|
||||
|
||||
public async Task<int> RevokeGrantsForOwnerAsync(long ownerPersonId, string reason, DateTime nowUtc,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
var items = _db.VaultItems.Where(i => i.OwnerPersonId == ownerPersonId).Select(i => i.Id);
|
||||
|
||||
var verleend = await _db.ContentGrants
|
||||
.Where(g => g.ResourceType == nameof(VaultItem) && items.Contains(g.ResourceId)
|
||||
&& g.Reason == reason && g.RevokedAt == null)
|
||||
.ToListAsync(ct);
|
||||
|
||||
foreach (var toegang in verleend) toegang.RevokedAt = nowUtc;
|
||||
return verleend.Count;
|
||||
}
|
||||
|
||||
public async Task RemoveGrantsAsync(long itemId, CancellationToken ct = default)
|
||||
{
|
||||
var verleend = await _db.ContentGrants
|
||||
.Where(g => g.ResourceType == nameof(VaultItem) && g.ResourceId == itemId)
|
||||
.ToListAsync(ct);
|
||||
_db.ContentGrants.RemoveRange(verleend);
|
||||
}
|
||||
}
|
||||
|
||||
public class MessageRepository : IMessageRepository
|
||||
{
|
||||
private readonly MijnBesluitDbContext _db;
|
||||
public MessageRepository(MijnBesluitDbContext db) => _db = db;
|
||||
|
||||
public Task<Message?> FindByIdAsync(long id, CancellationToken ct = default) =>
|
||||
_db.Messages.FirstOrDefaultAsync(m => m.Id == id, ct);
|
||||
|
||||
public async Task<IReadOnlyList<Message>> ListOutboxAsync(long fromPersonId, CancellationToken ct = default) =>
|
||||
await _db.Messages.Where(m => m.FromPersonId == fromPersonId && !m.HiddenBySender)
|
||||
.OrderByDescending(m => m.CreatedAt).ToListAsync(ct);
|
||||
|
||||
public async Task<IReadOnlyList<Message>> ListInboxAsync(long toPersonId, CancellationToken ct = default) =>
|
||||
await _db.Messages.Where(m => m.ToPersonId == toPersonId && !m.HiddenByRecipient &&
|
||||
(m.Status == MessageStatus.Bezorgd || m.Status == MessageStatus.Gelezen))
|
||||
.OrderByDescending(m => m.DeliveredAt).ToListAsync(ct);
|
||||
|
||||
/// <summary>
|
||||
/// Kandidaten voor bezorging: gepland, en óf er geen datum is (meteen), óf de datum is
|
||||
/// bereikt, óf het bericht wacht op een overlijden. De definitieve beoordeling doet
|
||||
/// MessageRelease.IsDue, met de afzender erbij.
|
||||
///
|
||||
/// Een bericht "meteen" heeft geen DeliverAt (null betekent direct, zie Message.cs) en geen
|
||||
/// DeliverAfterDeath - die viel hier eerder buiten de boot en bleef voor altijd "Gepland"
|
||||
/// staan, ook al was IsDue() er allang klaar voor.
|
||||
/// </summary>
|
||||
public async Task<IReadOnlyList<Message>> ListDueAsync(DateTime nowUtc, int max, CancellationToken ct = default) =>
|
||||
await _db.Messages
|
||||
.Where(m => m.Status == MessageStatus.Gepland &&
|
||||
(m.DeliverAfterDeath ||
|
||||
m.DeliverAt == null ||
|
||||
m.DeliverAt <= nowUtc))
|
||||
.OrderBy(m => m.DeliverAt)
|
||||
.Take(max)
|
||||
.ToListAsync(ct);
|
||||
|
||||
public async Task<IReadOnlyList<Message>> ListAwaitingDeathOfAsync(long fromPersonId, CancellationToken ct = default) =>
|
||||
await _db.Messages.Where(m => m.FromPersonId == fromPersonId && m.DeliverAfterDeath &&
|
||||
m.Status == MessageStatus.Gepland).ToListAsync(ct);
|
||||
|
||||
public async Task<IReadOnlyList<Message>> ListPlannedForAsync(long toPersonId, CancellationToken ct = default) =>
|
||||
await _db.Messages
|
||||
.Where(m => m.ToPersonId == toPersonId && m.Status == MessageStatus.Gepland)
|
||||
.OrderBy(m => m.DeliverAt)
|
||||
.ToListAsync(ct);
|
||||
|
||||
public async Task<IReadOnlyList<Message>> ListAllTouchingAsync(long personId, CancellationToken ct = default) =>
|
||||
await _db.Messages.Where(m => m.FromPersonId == personId || m.ToPersonId == personId).ToListAsync(ct);
|
||||
|
||||
public Task<Message?> FindByViewTokenHashAsync(byte[] tokenHash, CancellationToken ct = default) =>
|
||||
_db.Messages.FirstOrDefaultAsync(m => m.ViewTokenHash != null && m.ViewTokenHash == tokenHash, ct);
|
||||
|
||||
public void Add(Message message) => _db.Messages.Add(message);
|
||||
public void Remove(Message message) => _db.Messages.Remove(message);
|
||||
}
|
||||
|
||||
public class LegacyRepository : ILegacyRepository
|
||||
{
|
||||
private readonly MijnBesluitDbContext _db;
|
||||
public LegacyRepository(MijnBesluitDbContext db) => _db = db;
|
||||
|
||||
public async Task<IReadOnlyList<DeathDeclaration>> ListDeclarationsAsync(long personId, CancellationToken ct = default) =>
|
||||
await _db.DeathDeclarations.Where(d => d.PersonId == personId).ToListAsync(ct);
|
||||
|
||||
public Task<DeathDeclaration?> FindDeclarationByUserAsync(long personId, long userId, CancellationToken ct = default) =>
|
||||
_db.DeathDeclarations.FirstOrDefaultAsync(d => d.PersonId == personId && d.DeclaredByUserId == userId, ct);
|
||||
|
||||
public async Task<IReadOnlyList<LegacyManager>> ListManagersForAsync(long personId, CancellationToken ct = default) =>
|
||||
await _db.LegacyManagers.Where(m => m.PersonId == personId).OrderBy(m => m.Priority).ToListAsync(ct);
|
||||
|
||||
public async Task<IReadOnlyList<LegacyManager>> ListAssignmentsOfAsync(long managerPersonId, CancellationToken ct = default) =>
|
||||
await _db.LegacyManagers.Where(m => m.ManagerPersonId == managerPersonId).ToListAsync(ct);
|
||||
|
||||
public async Task<IReadOnlyList<DeathDeclaration>> ListAllDeclarationsAsync(long personId, CancellationToken ct = default) =>
|
||||
await _db.DeathDeclarations.Where(d => d.PersonId == personId).ToListAsync(ct);
|
||||
|
||||
public async Task<IReadOnlyList<DeathDeclaration>> ListDeclarationsByUserAsync(long userId, CancellationToken ct = default) =>
|
||||
await _db.DeathDeclarations.Where(d => d.DeclaredByUserId == userId).ToListAsync(ct);
|
||||
|
||||
public void Remove(DeathDeclaration declaration) => _db.DeathDeclarations.Remove(declaration);
|
||||
public void Add(DeathDeclaration declaration) => _db.DeathDeclarations.Add(declaration);
|
||||
public void Add(LegacyManager manager) => _db.LegacyManagers.Add(manager);
|
||||
public void Remove(LegacyManager manager) => _db.LegacyManagers.Remove(manager);
|
||||
}
|
||||
|
||||
public class SamenvoegRepository : ISamenvoegRepository
|
||||
{
|
||||
private readonly MijnBesluitDbContext _db;
|
||||
public SamenvoegRepository(MijnBesluitDbContext db) => _db = db;
|
||||
|
||||
public Task<Samenvoeging?> FindByIdAsync(long id, CancellationToken ct = default) =>
|
||||
_db.Samenvoegingen.FirstOrDefaultAsync(s => s.Id == id, ct);
|
||||
|
||||
public async Task<IReadOnlyList<Samenvoeging>> ListVoorDuplicaatAsync(long duplicaatPersonId,
|
||||
CancellationToken ct = default) =>
|
||||
await _db.Samenvoegingen.Where(s => s.DuplicaatPersonId == duplicaatPersonId)
|
||||
.OrderByDescending(s => s.SamengevoegdOp).ToListAsync(ct);
|
||||
|
||||
public async Task<IReadOnlyList<Samenvoeging>> ListVoorDoelAsync(long doelPersonId,
|
||||
CancellationToken ct = default) =>
|
||||
await _db.Samenvoegingen.Where(s => s.DoelPersonId == doelPersonId)
|
||||
.OrderByDescending(s => s.SamengevoegdOp).ToListAsync(ct);
|
||||
|
||||
public void Add(Samenvoeging samenvoeging) => _db.Samenvoegingen.Add(samenvoeging);
|
||||
}
|
||||
|
||||
/// <summary>Schrijft het logboek. Regels worden alleen toegevoegd, nooit gewijzigd.</summary>
|
||||
public class EfAuditLog : IAuditLog
|
||||
{
|
||||
private readonly MijnBesluitDbContext _db;
|
||||
private readonly IClock _clock;
|
||||
public EfAuditLog(MijnBesluitDbContext db, IClock clock) { _db = db; _clock = clock; }
|
||||
|
||||
/// <summary>Wordt per verzoek gevuld met het IP-adres en het toestel van de aanroeper.</summary>
|
||||
public string? IpAddress { get; set; }
|
||||
public string? UserAgent { get; set; }
|
||||
|
||||
public Task WriteAsync(AuditAction action, long? userId, string? entityType = null, long? entityId = null,
|
||||
string? details = null, CancellationToken ct = default)
|
||||
{
|
||||
_db.AuditLog.Add(new AuditLogEntry
|
||||
{
|
||||
Action = action,
|
||||
UserId = userId,
|
||||
EntityType = entityType,
|
||||
EntityId = entityId,
|
||||
Details = details,
|
||||
IpAddress = IpAddress,
|
||||
UserAgent = UserAgent,
|
||||
CreatedAt = _clock.UtcNow
|
||||
});
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Aanmeldingen van de app. Zie <see cref="Sessie"/> voor waarom ze in de database staan.
|
||||
///
|
||||
/// Er komt hier nooit een token binnen, alleen de hash ervan.
|
||||
/// </summary>
|
||||
public class SessieRepository : ISessieRepository
|
||||
{
|
||||
private readonly MijnBesluitDbContext _db;
|
||||
public SessieRepository(MijnBesluitDbContext db) => _db = db;
|
||||
|
||||
public Task<Sessie?> ZoekAsync(byte[] tokenHash, CancellationToken ct = default) =>
|
||||
_db.Sessies.FirstOrDefaultAsync(s => s.TokenHash == tokenHash, ct);
|
||||
|
||||
public void Add(Sessie sessie) => _db.Sessies.Add(sessie);
|
||||
|
||||
public async Task VerwijderAsync(byte[] tokenHash, CancellationToken ct = default)
|
||||
{
|
||||
var sessie = await _db.Sessies.FirstOrDefaultAsync(s => s.TokenHash == tokenHash, ct);
|
||||
if (sessie is not null) _db.Sessies.Remove(sessie);
|
||||
}
|
||||
|
||||
public async Task VerwijderAllesVanAsync(long userId, CancellationToken ct = default)
|
||||
{
|
||||
var sessies = await _db.Sessies.Where(s => s.UserId == userId).ToListAsync(ct);
|
||||
_db.Sessies.RemoveRange(sessies);
|
||||
}
|
||||
|
||||
public async Task<int> RuimVerlopenOpAsync(DateTime nu, CancellationToken ct = default)
|
||||
{
|
||||
var verlopen = await _db.Sessies.Where(s => s.VerlooptOp <= nu).ToListAsync(ct);
|
||||
_db.Sessies.RemoveRange(verlopen);
|
||||
return verlopen.Count;
|
||||
}
|
||||
}
|
||||
|
||||
public class ToestelRepository : IToestelRepository
|
||||
{
|
||||
private readonly MijnBesluitDbContext _db;
|
||||
public ToestelRepository(MijnBesluitDbContext db) => _db = db;
|
||||
|
||||
public Task<Toestel?> FindByKenmerkAsync(string kenmerk, CancellationToken ct = default) =>
|
||||
_db.Toestellen.FirstOrDefaultAsync(t => t.Kenmerk == kenmerk, ct);
|
||||
|
||||
public async Task<IReadOnlyList<Toestel>> ListForUserAsync(long userId, CancellationToken ct = default) =>
|
||||
await _db.Toestellen
|
||||
.Where(t => t.UserId == userId && t.AfgemeldOp == null)
|
||||
.OrderByDescending(t => t.LaatstGezien)
|
||||
.ToListAsync(ct);
|
||||
|
||||
public void Add(Toestel toestel) => _db.Toestellen.Add(toestel);
|
||||
public void Remove(Toestel toestel) => _db.Toestellen.Remove(toestel);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adverteerders, campagnes en de tellers per uur. Zie docs/Adverteren.md.
|
||||
/// </summary>
|
||||
public class AdvertentieRepository : IAdvertentieRepository
|
||||
{
|
||||
private readonly MijnBesluitDbContext _db;
|
||||
public AdvertentieRepository(MijnBesluitDbContext db) => _db = db;
|
||||
|
||||
public Task<Adverteerder?> FindAdverteerderAsync(long id, CancellationToken ct = default) =>
|
||||
_db.Adverteerders.FirstOrDefaultAsync(a => a.Id == id, ct);
|
||||
|
||||
public Task<Adverteerder?> FindAdverteerderByEmailAsync(string email, CancellationToken ct = default) =>
|
||||
_db.Adverteerders.FirstOrDefaultAsync(a => a.Email == email, ct);
|
||||
|
||||
public async Task<IReadOnlyList<Adverteerder>> ListAdverteerdersAsync(CancellationToken ct = default) =>
|
||||
await _db.Adverteerders.OrderBy(a => a.Bedrijfsnaam).ToListAsync(ct);
|
||||
|
||||
public void Add(Adverteerder adverteerder) => _db.Adverteerders.Add(adverteerder);
|
||||
|
||||
// De adverteerder hoort erbij: het beheerscherm laat campagnes zien met de bedrijfsnaam
|
||||
// ernaast, en zonder Include is die leeg.
|
||||
public Task<Campagne?> FindCampagneAsync(long id, CancellationToken ct = default) =>
|
||||
_db.Campagnes.Include(c => c.Adverteerder).FirstOrDefaultAsync(c => c.Id == id, ct);
|
||||
|
||||
public async Task<IReadOnlyList<Campagne>> ListCampagnesAsync(long adverteerderId,
|
||||
CancellationToken ct = default) =>
|
||||
await _db.Campagnes.Where(c => c.AdverteerderId == adverteerderId)
|
||||
.OrderByDescending(c => c.AangemaaktOp).ToListAsync(ct);
|
||||
|
||||
public async Task<IReadOnlyList<Campagne>> ListLopendAsync(DateOnly dag, CancellationToken ct = default) =>
|
||||
await _db.Campagnes
|
||||
.Where(c => c.Stand == CampagneStand.Loopt && c.VanafDatum <= dag && c.TotEnMetDatum >= dag)
|
||||
.ToListAsync(ct);
|
||||
|
||||
public async Task<IReadOnlyList<Campagne>> ListWachtendAsync(CancellationToken ct = default) =>
|
||||
await _db.Campagnes.Include(c => c.Adverteerder)
|
||||
.Where(c => c.Stand == CampagneStand.WachtOpBetaling ||
|
||||
c.Stand == CampagneStand.WachtOpGoedkeuring)
|
||||
.OrderBy(c => c.AangemaaktOp).ToListAsync(ct);
|
||||
|
||||
public void Add(Campagne campagne) => _db.Campagnes.Add(campagne);
|
||||
|
||||
public Task<CampagneUur?> FindUurAsync(long campagneId, DateTime uurUtc, CancellationToken ct = default) =>
|
||||
_db.CampagneUren.FirstOrDefaultAsync(u => u.CampagneId == campagneId && u.UurUtc == uurUtc, ct);
|
||||
|
||||
public async Task<IReadOnlyList<CampagneUur>> ListUrenAsync(long campagneId, CancellationToken ct = default) =>
|
||||
await _db.CampagneUren.Where(u => u.CampagneId == campagneId)
|
||||
.OrderBy(u => u.UurUtc).ToListAsync(ct);
|
||||
|
||||
public async Task<IReadOnlyDictionary<long, int>> VertoningenInUurAsync(
|
||||
DateTime uurUtc, IEnumerable<long> campagneIds, CancellationToken ct = default)
|
||||
{
|
||||
var lijst = campagneIds.ToList();
|
||||
if (lijst.Count == 0) return new Dictionary<long, int>();
|
||||
|
||||
return await _db.CampagneUren
|
||||
.Where(u => u.UurUtc == uurUtc && lijst.Contains(u.CampagneId))
|
||||
.ToDictionaryAsync(u => u.CampagneId, u => u.Vertoningen, ct);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Eén opdracht naar de database, want twee bezoekers tegelijk zouden anders allebei een
|
||||
/// nieuwe regel aanmaken en op de unieke sleutel stuklopen. ON DUPLICATE KEY UPDATE is van
|
||||
/// MariaDB/MySQL; dit project draait nergens anders op.
|
||||
/// </summary>
|
||||
public Task TelAsync(long campagneId, DateTime uurUtc, int vertoningen, int kliks,
|
||||
CancellationToken ct = default) =>
|
||||
_db.Database.ExecuteSqlInterpolatedAsync(
|
||||
$@"INSERT INTO campagne_uren (campaign_id, hour_utc, shown, clicks)
|
||||
VALUES ({campagneId}, {uurUtc}, {vertoningen}, {kliks})
|
||||
ON DUPLICATE KEY UPDATE shown = shown + {vertoningen}, clicks = clicks + {kliks}", ct);
|
||||
|
||||
public async Task<(int Vertoningen, int Kliks)> TotalenAsync(long campagneId, CancellationToken ct = default)
|
||||
{
|
||||
var uren = await _db.CampagneUren.Where(u => u.CampagneId == campagneId)
|
||||
.Select(u => new { u.Vertoningen, u.Kliks }).ToListAsync(ct);
|
||||
|
||||
return (uren.Sum(u => u.Vertoningen), uren.Sum(u => u.Kliks));
|
||||
}
|
||||
|
||||
public void Add(CampagneUur uur) => _db.CampagneUren.Add(uur);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tellingen voor de statistiektegel op de beheerpagina. Rechtstreeks op de context: dit zijn
|
||||
/// aantallen over de hele tabel heen, niet iets dat bij één van de andere repositories hoort.
|
||||
/// </summary>
|
||||
public class StatistiekRepository : IStatistiekRepository
|
||||
{
|
||||
private readonly MijnBesluitDbContext _db;
|
||||
public StatistiekRepository(MijnBesluitDbContext db) => _db = db;
|
||||
|
||||
public async Task<Statistieken> OphalenAsync(int groeidagen, DateTime nowUtc, CancellationToken ct = default)
|
||||
{
|
||||
// Samengevoegde profielen tellen niet mee: dat zijn duplicaten die zijn opgegaan in een
|
||||
// ander profiel, geen extra belasting.
|
||||
var profielen = await _db.Persons.CountAsync(p => p.MergedIntoPersonId == null, ct);
|
||||
var geclaimd = await _db.Persons.CountAsync(p => p.MergedIntoPersonId == null && p.ClaimedByUserId != null, ct);
|
||||
var families = await _db.Families.CountAsync(ct);
|
||||
var kluisItems = await _db.VaultItems.CountAsync(i => i.DeletedAt == null, ct);
|
||||
var kluisBestanden = await _db.VaultFiles.CountAsync(ct);
|
||||
var momenten = await _db.Messages.CountAsync(ct);
|
||||
var bezorgd = await _db.Messages.CountAsync(m => m.Status == MessageStatus.Bezorgd || m.Status == MessageStatus.Gelezen, ct);
|
||||
var beheerders = await _db.LegacyManagers.CountAsync(ct);
|
||||
|
||||
var perCategorie = await _db.VaultItems.Where(i => i.DeletedAt == null)
|
||||
.GroupBy(i => i.Category)
|
||||
.Select(g => new { Categorie = g.Key, Aantal = g.Count() })
|
||||
.ToListAsync(ct);
|
||||
|
||||
// De groeigrafiek: nieuwe profielen en nieuwe kluisitems per dag, de laatste
|
||||
// `groeidagen` dagen. Twee losse groeperingen en dan samenvoegen, want het zijn twee
|
||||
// verschillende tabellen.
|
||||
var vanaf = nowUtc.Date.AddDays(-(groeidagen - 1));
|
||||
|
||||
var nieuweProfielenPerDag = await _db.Persons
|
||||
.Where(p => p.CreatedAt >= vanaf && p.MergedIntoPersonId == null)
|
||||
.GroupBy(p => p.CreatedAt.Date)
|
||||
.Select(g => new { Datum = g.Key, Aantal = g.Count() })
|
||||
.ToListAsync(ct);
|
||||
|
||||
var nieuweItemsPerDag = await _db.VaultItems
|
||||
.Where(i => i.CreatedAt >= vanaf)
|
||||
.GroupBy(i => i.CreatedAt.Date)
|
||||
.Select(g => new { Datum = g.Key, Aantal = g.Count() })
|
||||
.ToListAsync(ct);
|
||||
|
||||
var profielenPerDatum = nieuweProfielenPerDag.ToDictionary(x => DateOnly.FromDateTime(x.Datum), x => x.Aantal);
|
||||
var itemsPerDatum = nieuweItemsPerDag.ToDictionary(x => DateOnly.FromDateTime(x.Datum), x => x.Aantal);
|
||||
|
||||
var groei = new List<GroeiPunt>();
|
||||
for (var d = DateOnly.FromDateTime(vanaf); d <= DateOnly.FromDateTime(nowUtc.Date); d = d.AddDays(1))
|
||||
{
|
||||
groei.Add(new GroeiPunt(d,
|
||||
profielenPerDatum.TryGetValue(d, out var np) ? np : 0,
|
||||
itemsPerDatum.TryGetValue(d, out var ni) ? ni : 0));
|
||||
}
|
||||
|
||||
return new Statistieken(profielen, geclaimd, families, kluisItems, kluisBestanden, momenten, bezorgd,
|
||||
beheerders, perCategorie.ToDictionary(x => x.Categorie, x => x.Aantal), groei);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user