diff --git a/src/Digdir.Domain.Dialogporten.Application/Common/Extensions/ClaimsPrincipalExtensions.cs b/src/Digdir.Domain.Dialogporten.Application/Common/Extensions/ClaimsPrincipalExtensions.cs index c78f6cf88..4ef0ac234 100644 --- a/src/Digdir.Domain.Dialogporten.Application/Common/Extensions/ClaimsPrincipalExtensions.cs +++ b/src/Digdir.Domain.Dialogporten.Application/Common/Extensions/ClaimsPrincipalExtensions.cs @@ -15,10 +15,30 @@ public static class ClaimsPrincipalExtensions private const char IdDelimiter = ':'; private const string IdPrefix = "0192"; private const string OrgClaim = "urn:altinn:org"; + private const string PidClaim = "pid"; public static bool TryGetOrgNumber(this ClaimsPrincipal claimsPrincipal, [NotNullWhen(true)] out string? orgNumber) => claimsPrincipal.FindFirst(ConsumerClaim).TryGetOrgNumber(out orgNumber); + public static bool TryGetPid(this ClaimsPrincipal claimsPrincipal, [NotNullWhen(true)] out string? pid) + => claimsPrincipal.FindFirst(PidClaim).TryGetPid(out pid); + + public static bool TryGetPid(this Claim? pidClaim, [NotNullWhen(true)] out string? pid) + { + pid = null; + if (pidClaim is null || pidClaim.Type != PidClaim) + { + return false; + } + + if (NorwegianPersonIdentifier.IsValid(pidClaim.Value)) + { + pid = pidClaim.Value; + } + + return pid is not null; + } + public static bool TryGetOrgNumber(this Claim? consumerClaim, [NotNullWhen(true)] out string? orgNumber) { orgNumber = null; @@ -54,10 +74,10 @@ public static bool TryGetOrgNumber(this Claim? consumerClaim, [NotNullWhen(true) return orgNumber is not null; } - public static bool TryGetOrgShortName(this ClaimsPrincipal claimsPrincipal, [NotNullWhen(true)] out string? orgShortName) + private static bool TryGetOrgShortName(this ClaimsPrincipal claimsPrincipal, [NotNullWhen(true)] out string? orgShortName) => claimsPrincipal.FindFirst(OrgClaim).TryGetOrgShortName(out orgShortName); - public static bool TryGetOrgShortName(this Claim? orgClaim, [NotNullWhen(true)] out string? orgShortName) + private static bool TryGetOrgShortName(this Claim? orgClaim, [NotNullWhen(true)] out string? orgShortName) { orgShortName = orgClaim?.Value; @@ -69,4 +89,7 @@ internal static bool TryGetOrgNumber(this IUser user, [NotNullWhen(true)] out st internal static bool TryGetOrgShortName(this IUser user, [NotNullWhen(true)] out string? orgShortName) => user.GetPrincipal().TryGetOrgShortName(out orgShortName); + + internal static bool TryGetPid(this IUser user, [NotNullWhen(true)] out string? pid) => + user.GetPrincipal().TryGetPid(out pid); } diff --git a/src/Digdir.Domain.Dialogporten.Application/Common/IUserService.cs b/src/Digdir.Domain.Dialogporten.Application/Common/IUserService.cs index 5f6aabb9c..714001770 100644 --- a/src/Digdir.Domain.Dialogporten.Application/Common/IUserService.cs +++ b/src/Digdir.Domain.Dialogporten.Application/Common/IUserService.cs @@ -1,6 +1,7 @@ using Digdir.Domain.Dialogporten.Application.Externals.Presentation; using Digdir.Domain.Dialogporten.Application.Externals; using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; using Digdir.Domain.Dialogporten.Application.Common.Extensions; namespace Digdir.Domain.Dialogporten.Application.Common; @@ -10,6 +11,8 @@ internal interface IUserService Task CurrentUserIsOwner(string serviceResource, CancellationToken cancellationToken); Task> GetCurrentUserResourceIds(CancellationToken cancellationToken); Task GetCurrentUserOrgShortName(CancellationToken cancellationToken); + bool TryGetCurrentUserPid([NotNullWhen(true)] out string? userPid); + Task GetCurrentUserName(string personalIdentificationNumber, CancellationToken cancellationToken); } internal sealed class UserService : IUserService @@ -17,15 +20,18 @@ internal sealed class UserService : IUserService private readonly IUser _user; private readonly IResourceRegistry _resourceRegistry; private readonly IOrganizationRegistry _organizationRegistry; + private readonly INameRegistry _nameRegistry; public UserService( IUser user, IResourceRegistry resourceRegistry, - IOrganizationRegistry organizationRegistry) + IOrganizationRegistry organizationRegistry, + INameRegistry nameRegistry) { _user = user ?? throw new ArgumentNullException(nameof(user)); _resourceRegistry = resourceRegistry ?? throw new ArgumentNullException(nameof(resourceRegistry)); _organizationRegistry = organizationRegistry ?? throw new ArgumentNullException(nameof(organizationRegistry)); + _nameRegistry = nameRegistry ?? throw new ArgumentNullException(nameof(nameRegistry)); } public async Task CurrentUserIsOwner(string serviceResource, CancellationToken cancellationToken) @@ -35,10 +41,15 @@ public async Task CurrentUserIsOwner(string serviceResource, CancellationT } public Task> GetCurrentUserResourceIds(CancellationToken cancellationToken) => - !_user.TryGetOrgNumber(out var orgNumber) + !_user.TryGetOrgNumber(out var orgNumber) ? throw new UnreachableException() : _resourceRegistry.GetResourceIds(orgNumber, cancellationToken); + public bool TryGetCurrentUserPid([NotNullWhen(true)] out string? userPid) => _user.TryGetPid(out userPid); + + public async Task GetCurrentUserName(string personalIdentificationNumber, + CancellationToken cancellationToken) => await _nameRegistry.GetName(personalIdentificationNumber, cancellationToken); + public async Task GetCurrentUserOrgShortName(CancellationToken cancellationToken) { if (_user.TryGetOrgShortName(out var orgShortName)) @@ -72,4 +83,11 @@ public Task> GetCurrentUserResourceIds(CancellationT public Task GetCurrentUserOrgShortName(CancellationToken cancellationToken) => _userService.GetCurrentUserOrgShortName(cancellationToken); + + public bool TryGetCurrentUserPid([NotNullWhen(true)] out string? userPid) => + _userService.TryGetCurrentUserPid(out userPid); + + + public async Task GetCurrentUserName(string personalIdentificationNumber, CancellationToken cancellationToken) + => await Task.FromResult("Local Development User"); } diff --git a/src/Digdir.Domain.Dialogporten.Application/Common/MappingUtils.cs b/src/Digdir.Domain.Dialogporten.Application/Common/MappingUtils.cs new file mode 100644 index 000000000..effa6a705 --- /dev/null +++ b/src/Digdir.Domain.Dialogporten.Application/Common/MappingUtils.cs @@ -0,0 +1,26 @@ +using System.Security.Cryptography; +using System.Text; + +namespace Digdir.Domain.Dialogporten.Application.Common; + +internal static class MappingUtils +{ + internal static byte[] GetHashSalt(int size = 16) => RandomNumberGenerator.GetBytes(size); + + internal static string? HashPid(string? personIdentifier, byte[] salt) + { + if (string.IsNullOrWhiteSpace(personIdentifier)) + { + return null; + } + + var identifierBytes = Encoding.UTF8.GetBytes(personIdentifier); + Span buffer = stackalloc byte[identifierBytes.Length + salt.Length]; + identifierBytes.CopyTo(buffer); + salt.CopyTo(buffer[identifierBytes.Length..]); + + var hashBytes = SHA256.HashData(buffer); + + return BitConverter.ToString(hashBytes, 0, 5).Replace("-", "").ToLowerInvariant(); + } +} diff --git a/src/Digdir.Domain.Dialogporten.Application/Externals/INameRegistry.cs b/src/Digdir.Domain.Dialogporten.Application/Externals/INameRegistry.cs new file mode 100644 index 000000000..1af576e79 --- /dev/null +++ b/src/Digdir.Domain.Dialogporten.Application/Externals/INameRegistry.cs @@ -0,0 +1,6 @@ +namespace Digdir.Domain.Dialogporten.Application.Externals; + +public interface INameRegistry +{ + Task GetName(string personalIdentificationNumber, CancellationToken cancellationToken); +} diff --git a/src/Digdir.Domain.Dialogporten.Application/Features/V1/Common/Events/CloudEventTypes.cs b/src/Digdir.Domain.Dialogporten.Application/Features/V1/Common/Events/CloudEventTypes.cs index adb782103..43a10f4dc 100644 --- a/src/Digdir.Domain.Dialogporten.Application/Features/V1/Common/Events/CloudEventTypes.cs +++ b/src/Digdir.Domain.Dialogporten.Application/Features/V1/Common/Events/CloudEventTypes.cs @@ -12,7 +12,7 @@ internal static class CloudEventTypes DialogCreatedDomainEvent => "dialogporten.dialog.created.v1", DialogUpdatedDomainEvent => "dialogporten.dialog.updated.v1", DialogDeletedDomainEvent => "dialogporten.dialog.deleted.v1", - DialogReadDomainEvent => "dialogporten.dialog.read.v1", + DialogSeenDomainEvent => "dialogporten.dialog.seen.v1", // DialogElement DialogElementDeletedDomainEvent => "dialogporten.dialog.element.deleted.v1", diff --git a/src/Digdir.Domain.Dialogporten.Application/Features/V1/Common/Events/DialogEventToAltinnForwarder.cs b/src/Digdir.Domain.Dialogporten.Application/Features/V1/Common/Events/DialogEventToAltinnForwarder.cs index 180136642..036dd0449 100644 --- a/src/Digdir.Domain.Dialogporten.Application/Features/V1/Common/Events/DialogEventToAltinnForwarder.cs +++ b/src/Digdir.Domain.Dialogporten.Application/Features/V1/Common/Events/DialogEventToAltinnForwarder.cs @@ -11,7 +11,7 @@ internal sealed class DialogEventToAltinnForwarder : DomainEventToAltinnForwarde INotificationHandler, INotificationHandler, INotificationHandler, - INotificationHandler + INotificationHandler { public DialogEventToAltinnForwarder(ICloudEventBus cloudEventBus, IDialogDbContext db, IOptions settings) @@ -31,7 +31,7 @@ public async Task Handle(DialogUpdatedDomainEvent domainEvent, CancellationToken await CloudEventBus.Publish(cloudEvent, cancellationToken); } - public async Task Handle(DialogReadDomainEvent domainEvent, CancellationToken cancellationToken) + public async Task Handle(DialogSeenDomainEvent domainEvent, CancellationToken cancellationToken) { var dialog = await GetDialog(domainEvent.DialogId, cancellationToken); var cloudEvent = CreateCloudEvent(domainEvent, dialog); diff --git a/src/Digdir.Domain.Dialogporten.Application/Features/V1/EndUser/DialogActivities/Queries/Get/GetDialogActivityDto.cs b/src/Digdir.Domain.Dialogporten.Application/Features/V1/EndUser/DialogActivities/Queries/Get/GetDialogActivityDto.cs index dd108bc07..66560abbf 100644 --- a/src/Digdir.Domain.Dialogporten.Application/Features/V1/EndUser/DialogActivities/Queries/Get/GetDialogActivityDto.cs +++ b/src/Digdir.Domain.Dialogporten.Application/Features/V1/EndUser/DialogActivities/Queries/Get/GetDialogActivityDto.cs @@ -8,6 +8,7 @@ public sealed class GetDialogActivityDto public Guid Id { get; set; } public DateTimeOffset? CreatedAt { get; set; } public Uri? ExtendedType { get; set; } + public string? SeenByEndUserIdHash { get; init; } public DialogActivityType.Values Type { get; set; } @@ -16,4 +17,4 @@ public sealed class GetDialogActivityDto public List? PerformedBy { get; set; } = new(); public List Description { get; set; } = new(); -} \ No newline at end of file +} diff --git a/src/Digdir.Domain.Dialogporten.Application/Features/V1/EndUser/DialogActivities/Queries/Get/GetDialogActivityQuery.cs b/src/Digdir.Domain.Dialogporten.Application/Features/V1/EndUser/DialogActivities/Queries/Get/GetDialogActivityQuery.cs index 589d9b0c7..96b2c6070 100644 --- a/src/Digdir.Domain.Dialogporten.Application/Features/V1/EndUser/DialogActivities/Queries/Get/GetDialogActivityQuery.cs +++ b/src/Digdir.Domain.Dialogporten.Application/Features/V1/EndUser/DialogActivities/Queries/Get/GetDialogActivityQuery.cs @@ -1,4 +1,5 @@ using AutoMapper; +using Digdir.Domain.Dialogporten.Application.Common; using Digdir.Domain.Dialogporten.Application.Common.ReturnTypes; using Digdir.Domain.Dialogporten.Application.Externals; using Digdir.Domain.Dialogporten.Application.Externals.AltinnAuthorization; @@ -73,6 +74,12 @@ public async Task Handle(GetDialogActivityQuery request return new EntityNotFound(request.ActivityId); } + // Hash end user id + if (activity.SeenByEndUserId is not null) + { + activity.SeenByEndUserId = MappingUtils.HashPid(activity.SeenByEndUserId, MappingUtils.GetHashSalt()); + } + return _mapper.Map(activity); } } diff --git a/src/Digdir.Domain.Dialogporten.Application/Features/V1/EndUser/DialogActivities/Queries/Get/MappingProfile.cs b/src/Digdir.Domain.Dialogporten.Application/Features/V1/EndUser/DialogActivities/Queries/Get/MappingProfile.cs index 0e30f1f35..d4116efbf 100644 --- a/src/Digdir.Domain.Dialogporten.Application/Features/V1/EndUser/DialogActivities/Queries/Get/MappingProfile.cs +++ b/src/Digdir.Domain.Dialogporten.Application/Features/V1/EndUser/DialogActivities/Queries/Get/MappingProfile.cs @@ -3,11 +3,12 @@ namespace Digdir.Domain.Dialogporten.Application.Features.V1.EndUser.DialogActivities.Queries.Get; -public class MappingProfile : Profile +internal sealed class MappingProfile : Profile { public MappingProfile() { CreateMap() + .ForMember(dest => dest.SeenByEndUserIdHash, opt => opt.MapFrom(src => src.SeenByEndUserId)) .ForMember(dest => dest.Type, opt => opt.MapFrom(src => src.TypeId)); } } diff --git a/src/Digdir.Domain.Dialogporten.Application/Features/V1/EndUser/DialogActivities/Queries/Search/MappingProfile.cs b/src/Digdir.Domain.Dialogporten.Application/Features/V1/EndUser/DialogActivities/Queries/Search/MappingProfile.cs index d5346c394..1040028e5 100644 --- a/src/Digdir.Domain.Dialogporten.Application/Features/V1/EndUser/DialogActivities/Queries/Search/MappingProfile.cs +++ b/src/Digdir.Domain.Dialogporten.Application/Features/V1/EndUser/DialogActivities/Queries/Search/MappingProfile.cs @@ -8,7 +8,7 @@ internal sealed class MappingProfile : Profile public MappingProfile() { CreateMap() - .ForMember(dest => dest.Type, - opt => opt.MapFrom(src => src.TypeId)); + .ForMember(dest => dest.SeenByEndUserIdHash, opt => opt.MapFrom(src => src.SeenByEndUserId)) + .ForMember(dest => dest.Type, opt => opt.MapFrom(src => src.TypeId)); } -} \ No newline at end of file +} diff --git a/src/Digdir.Domain.Dialogporten.Application/Features/V1/EndUser/DialogActivities/Queries/Search/SearchDialogActivityDto.cs b/src/Digdir.Domain.Dialogporten.Application/Features/V1/EndUser/DialogActivities/Queries/Search/SearchDialogActivityDto.cs index adce19ec6..311cddeeb 100644 --- a/src/Digdir.Domain.Dialogporten.Application/Features/V1/EndUser/DialogActivities/Queries/Search/SearchDialogActivityDto.cs +++ b/src/Digdir.Domain.Dialogporten.Application/Features/V1/EndUser/DialogActivities/Queries/Search/SearchDialogActivityDto.cs @@ -7,6 +7,7 @@ public sealed class SearchDialogActivityDto public Guid Id { get; set; } public DateTimeOffset CreatedAt { get; set; } public Uri? ExtendedType { get; set; } + public string? SeenByEndUserIdHash { get; set; } public DialogActivityType.Values Type { get; set; } diff --git a/src/Digdir.Domain.Dialogporten.Application/Features/V1/EndUser/DialogActivities/Queries/Search/SearchDialogActivityQuery.cs b/src/Digdir.Domain.Dialogporten.Application/Features/V1/EndUser/DialogActivities/Queries/Search/SearchDialogActivityQuery.cs index 3da553e5d..7bffedbdf 100644 --- a/src/Digdir.Domain.Dialogporten.Application/Features/V1/EndUser/DialogActivities/Queries/Search/SearchDialogActivityQuery.cs +++ b/src/Digdir.Domain.Dialogporten.Application/Features/V1/EndUser/DialogActivities/Queries/Search/SearchDialogActivityQuery.cs @@ -1,4 +1,5 @@ using AutoMapper; +using Digdir.Domain.Dialogporten.Application.Common; using Digdir.Domain.Dialogporten.Application.Common.ReturnTypes; using Digdir.Domain.Dialogporten.Application.Externals; using Digdir.Domain.Dialogporten.Application.Externals.AltinnAuthorization; @@ -61,6 +62,13 @@ public async Task Handle(SearchDialogActivityQuery r return new EntityDeleted(request.DialogId); } + // hash end user ids + var salt = MappingUtils.GetHashSalt(); + foreach (var activity in dialog.Activities) + { + activity.SeenByEndUserId = MappingUtils.HashPid(activity.SeenByEndUserId, salt); + } + return _mapper.Map>(dialog.Activities); } } diff --git a/src/Digdir.Domain.Dialogporten.Application/Features/V1/EndUser/Dialogs/Queries/Get/GetDialogDto.cs b/src/Digdir.Domain.Dialogporten.Application/Features/V1/EndUser/Dialogs/Queries/Get/GetDialogDto.cs index 15acfe4ef..d866f6bc4 100644 --- a/src/Digdir.Domain.Dialogporten.Application/Features/V1/EndUser/Dialogs/Queries/Get/GetDialogDto.cs +++ b/src/Digdir.Domain.Dialogporten.Application/Features/V1/EndUser/Dialogs/Queries/Get/GetDialogDto.cs @@ -46,6 +46,7 @@ public sealed class GetDialogDialogActivityDto public Guid Id { get; set; } public DateTimeOffset? CreatedAt { get; set; } public Uri? ExtendedType { get; set; } + public string? SeenByEndUserIdHash { get; init; } public DialogActivityType.Values Type { get; set; } diff --git a/src/Digdir.Domain.Dialogporten.Application/Features/V1/EndUser/Dialogs/Queries/Get/GetDialogQuery.cs b/src/Digdir.Domain.Dialogporten.Application/Features/V1/EndUser/Dialogs/Queries/Get/GetDialogQuery.cs index 99d681f0a..6e0a0cd85 100644 --- a/src/Digdir.Domain.Dialogporten.Application/Features/V1/EndUser/Dialogs/Queries/Get/GetDialogQuery.cs +++ b/src/Digdir.Domain.Dialogporten.Application/Features/V1/EndUser/Dialogs/Queries/Get/GetDialogQuery.cs @@ -18,35 +18,40 @@ public sealed class GetDialogQuery : IRequest } [GenerateOneOf] -public partial class GetDialogResult : OneOfBase { } +public partial class GetDialogResult : OneOfBase; internal sealed class GetDialogQueryHandler : IRequestHandler { private readonly IDialogDbContext _db; private readonly IMapper _mapper; private readonly IUnitOfWork _unitOfWork; - private readonly ITransactionTime _transactionTime; private readonly IClock _clock; + private readonly IUserService _userService; private readonly IAltinnAuthorization _altinnAuthorization; public GetDialogQueryHandler( IDialogDbContext db, IMapper mapper, IUnitOfWork unitOfWork, - ITransactionTime transactionTime, IClock clock, + IUserService userService, IAltinnAuthorization altinnAuthorization) { _db = db ?? throw new ArgumentNullException(nameof(db)); _mapper = mapper ?? throw new ArgumentNullException(nameof(mapper)); _unitOfWork = unitOfWork ?? throw new ArgumentNullException(nameof(unitOfWork)); - _transactionTime = transactionTime ?? throw new ArgumentNullException(nameof(transactionTime)); _clock = clock ?? throw new ArgumentNullException(nameof(clock)); + _userService = userService ?? throw new ArgumentNullException(nameof(userService)); _altinnAuthorization = altinnAuthorization ?? throw new ArgumentNullException(nameof(altinnAuthorization)); } public async Task Handle(GetDialogQuery request, CancellationToken cancellationToken) { + if (!_userService.TryGetCurrentUserPid(out var userPid)) + { + return new Forbidden("No valid user pid found."); + } + // This query could be written without all the includes as ProjectTo will do the job for us. // However, we need to guarantee an order for sub resources of the dialog aggregate. // This is to ensure that the get is consistent, and that PATCH in the API presentation @@ -87,7 +92,10 @@ public async Task Handle(GetDialogQuery request, CancellationTo return new EntityDeleted(request.DialogId); } - dialog.UpdateReadAt(_transactionTime.Value); + var userName = await _userService.GetCurrentUserName(userPid, cancellationToken); + // TODO: What if name lookup fails + // https://github.com/digdir/dialogporten/issues/387 + dialog.UpdateSeenAt(userPid, userName); var saveResult = await _unitOfWork .WithoutAuditableSideEffects() @@ -98,6 +106,13 @@ public async Task Handle(GetDialogQuery request, CancellationTo domainError => throw new UnreachableException("Should not get domain error when updating ReadAt."), concurrencyError => throw new UnreachableException("Should not get concurrencyError when updating ReadAt.")); + // hash end user ids + var salt = MappingUtils.GetHashSalt(); + foreach (var activity in dialog.Activities) + { + activity.SeenByEndUserId = MappingUtils.HashPid(activity.SeenByEndUserId, salt); + } + var dto = _mapper.Map(dialog); DecorateWithAuthorization(dto, authorizationResult); diff --git a/src/Digdir.Domain.Dialogporten.Application/Features/V1/EndUser/Dialogs/Queries/Get/MappingProfile.cs b/src/Digdir.Domain.Dialogporten.Application/Features/V1/EndUser/Dialogs/Queries/Get/MappingProfile.cs index 9f88bd326..cdf6ae997 100644 --- a/src/Digdir.Domain.Dialogporten.Application/Features/V1/EndUser/Dialogs/Queries/Get/MappingProfile.cs +++ b/src/Digdir.Domain.Dialogporten.Application/Features/V1/EndUser/Dialogs/Queries/Get/MappingProfile.cs @@ -15,6 +15,7 @@ public MappingProfile() .ForMember(dest => dest.Status, opt => opt.MapFrom(src => src.StatusId)); CreateMap() + .ForMember(dest => dest.SeenByEndUserIdHash, opt => opt.MapFrom(src => src.SeenByEndUserId)) .ForMember(dest => dest.Type, opt => opt.MapFrom(src => src.TypeId)); CreateMap(); @@ -33,4 +34,5 @@ public MappingProfile() CreateMap() .ForMember(dest => dest.Type, opt => opt.MapFrom(src => src.TypeId)); } + } diff --git a/src/Digdir.Domain.Dialogporten.Application/Features/V1/EndUser/Dialogs/Queries/Search/MappingProfile.cs b/src/Digdir.Domain.Dialogporten.Application/Features/V1/EndUser/Dialogs/Queries/Search/MappingProfile.cs index f88baded6..9a907fa31 100644 --- a/src/Digdir.Domain.Dialogporten.Application/Features/V1/EndUser/Dialogs/Queries/Search/MappingProfile.cs +++ b/src/Digdir.Domain.Dialogporten.Application/Features/V1/EndUser/Dialogs/Queries/Search/MappingProfile.cs @@ -1,5 +1,6 @@ using AutoMapper; using Digdir.Domain.Dialogporten.Domain.Dialogs.Entities; +using Digdir.Domain.Dialogporten.Domain.Dialogs.Entities.Activities; using Digdir.Domain.Dialogporten.Domain.Dialogs.Entities.Content; namespace Digdir.Domain.Dialogporten.Application.Features.V1.EndUser.Dialogs.Queries.Search; @@ -9,10 +10,15 @@ internal sealed class MappingProfile : Profile public MappingProfile() { CreateMap() + .ForMember(dest => dest.LatestActivities, opt => opt.Ignore()) .ForMember(dest => dest.Content, opt => opt.MapFrom(src => src.Content.Where(x => x.Type.OutputInList))) .ForMember(dest => dest.Status, opt => opt.MapFrom(src => src.StatusId)); CreateMap() .ForMember(dest => dest.Type, opt => opt.MapFrom(src => src.TypeId)); + + CreateMap() + .ForMember(dest => dest.SeenByEndUserIdHash, opt => opt.MapFrom(src => src.SeenByEndUserId)) + .ForMember(dest => dest.Type, opt => opt.MapFrom(src => src.TypeId)); } } diff --git a/src/Digdir.Domain.Dialogporten.Application/Features/V1/EndUser/Dialogs/Queries/Search/SearchDialogDto.cs b/src/Digdir.Domain.Dialogporten.Application/Features/V1/EndUser/Dialogs/Queries/Search/SearchDialogDto.cs index e78a1a3e9..802f91aff 100644 --- a/src/Digdir.Domain.Dialogporten.Application/Features/V1/EndUser/Dialogs/Queries/Search/SearchDialogDto.cs +++ b/src/Digdir.Domain.Dialogporten.Application/Features/V1/EndUser/Dialogs/Queries/Search/SearchDialogDto.cs @@ -1,5 +1,6 @@ using Digdir.Domain.Dialogporten.Application.Features.V1.Common.Localizations; using Digdir.Domain.Dialogporten.Domain.Dialogs.Entities; +using Digdir.Domain.Dialogporten.Domain.Dialogs.Entities.Activities; using Digdir.Domain.Dialogporten.Domain.Dialogs.Entities.Content; namespace Digdir.Domain.Dialogporten.Application.Features.V1.EndUser.Dialogs.Queries.Search; @@ -18,11 +19,30 @@ public sealed class SearchDialogDto public DialogStatus.Values Status { get; set; } - public List Content { get; set; } = new(); + public List Content { get; set; } = []; + public List LatestActivities { get; set; } = []; + } public sealed class SearchDialogContentDto { public DialogContentType.Values Type { get; set; } - public List Value { get; set; } = new(); + public List Value { get; set; } = []; +} + +public sealed class SearchDialogDialogActivityDto +{ + public Guid Id { get; set; } + public DateTimeOffset? CreatedAt { get; set; } + public Uri? ExtendedType { get; set; } + public string? SeenByEndUserIdHash { get; set; } + public bool? SeenActivityIsCurrentEndUser { get; set; } + + public DialogActivityType.Values Type { get; set; } + + public Guid? RelatedActivityId { get; set; } + public Guid? DialogElementId { get; set; } + + public List? PerformedBy { get; set; } = []; + public List Description { get; set; } = []; } diff --git a/src/Digdir.Domain.Dialogporten.Application/Features/V1/EndUser/Dialogs/Queries/Search/SearchDialogQuery.cs b/src/Digdir.Domain.Dialogporten.Application/Features/V1/EndUser/Dialogs/Queries/Search/SearchDialogQuery.cs index 855a4d17f..50732ee0f 100644 --- a/src/Digdir.Domain.Dialogporten.Application/Features/V1/EndUser/Dialogs/Queries/Search/SearchDialogQuery.cs +++ b/src/Digdir.Domain.Dialogporten.Application/Features/V1/EndUser/Dialogs/Queries/Search/SearchDialogQuery.cs @@ -10,6 +10,7 @@ using Digdir.Domain.Dialogporten.Application.Externals; using Digdir.Domain.Dialogporten.Application.Externals.AltinnAuthorization; using Digdir.Domain.Dialogporten.Domain.Dialogs.Entities; +using Digdir.Domain.Dialogporten.Domain.Dialogs.Entities.Activities; using Digdir.Domain.Dialogporten.Domain.Localizations; using MediatR; using Microsoft.EntityFrameworkCore; @@ -107,33 +108,41 @@ public static IOrderOptions Configure(IOrderOptionsBuilder, ValidationError> { } +public partial class SearchDialogResult : OneOfBase, ValidationError, Forbidden>; internal sealed class SearchDialogQueryHandler : IRequestHandler { private readonly IDialogDbContext _db; private readonly IMapper _mapper; private readonly IClock _clock; + private readonly IUserService _userService; private readonly IAltinnAuthorization _altinnAuthorization; public SearchDialogQueryHandler( IDialogDbContext db, IMapper mapper, IClock clock, + IUserService userService, IAltinnAuthorization altinnAuthorization) { _db = db ?? throw new ArgumentNullException(nameof(db)); _mapper = mapper ?? throw new ArgumentNullException(nameof(mapper)); _clock = clock ?? throw new ArgumentNullException(nameof(clock)); + _userService = userService ?? throw new ArgumentNullException(nameof(userService)); _altinnAuthorization = altinnAuthorization ?? throw new ArgumentNullException(nameof(altinnAuthorization)); } public async Task Handle(SearchDialogQuery request, CancellationToken cancellationToken) { + if (!_userService.TryGetCurrentUserPid(out var userPid)) + { + return new Forbidden("No valid user pid found."); + } + var searchExpression = Expressions.LocalizedSearchExpression(request.Search, request.SearchCultureCode); var authorizedResources = await _altinnAuthorization.GetAuthorizedResourcesForSearch( - request.Party ?? new List(), - request.ServiceResource ?? new List(), + request.Party ?? [], + request.ServiceResource ?? [], cancellationToken: cancellationToken); if (authorizedResources.HasNoAuthorizations) @@ -141,7 +150,8 @@ public async Task Handle(SearchDialogQuery request, Cancella return new PaginatedList(Enumerable.Empty(), false, null, request.OrderBy.DefaultIfNull().GetOrderString()); } - return await _db.Dialogs + var paginatedList = await _db.Dialogs + .AsNoTracking() .WhereUserIsAuthorizedFor(authorizedResources) .WhereIf(!request.Org.IsNullOrEmpty(), x => request.Org!.Contains(x.Org)) .WhereIf(!request.ServiceResource.IsNullOrEmpty(), x => request.ServiceResource!.Contains(x.ServiceResource)) @@ -164,5 +174,67 @@ public async Task Handle(SearchDialogQuery request, Cancella .Where(x => !x.ExpiresAt.HasValue || x.ExpiresAt > _clock.UtcNowOffset) .ProjectTo(_mapper.ConfigurationProvider) .ToPaginatedListAsync(request, cancellationToken: cancellationToken); + + await FetchRelevantActivities(paginatedList, userPid, cancellationToken); + + return paginatedList; + } + + private async Task FetchRelevantActivities(PaginatedList paginatedList, string userPid, CancellationToken cancellationToken) + { + var dialogIds = paginatedList.Items + .Select(x => x.Id) + .ToList(); + + var latestActivityByDialogIdTask = await _db.DialogActivities + .AsNoTracking() + .Include(x => x.Description!.Localizations) + .Include(x => x.PerformedBy!.Localizations) + .Where(x => + dialogIds.Contains(x.DialogId) + && x.TypeId != DialogActivityType.Values.Forwarded + && x.TypeId != DialogActivityType.Values.Seen) + .GroupBy(x => x.DialogId) + .ToDictionaryAsync( + x => x.Key, + x => x.OrderByDescending(x => x.CreatedAt) + .ThenBy(x => x.Id) + .First(), + cancellationToken); + + var latestSeenActivityByDialogIdTask = await _db.DialogActivities + .AsNoTracking() + .Include(x => x.Description!.Localizations) + .Include(x => x.PerformedBy!.Localizations) + .Where(x => + dialogIds.Contains(x.DialogId) + && x.TypeId == DialogActivityType.Values.Seen + && x.CreatedAt > x.Dialog.UpdatedAt) + .GroupBy(x => x.DialogId) + .ToDictionaryAsync(x => x.Key, x => x.ToList(), cancellationToken); + + var salt = MappingUtils.GetHashSalt(); + foreach (var dialog in paginatedList.Items) + { + var activities = latestSeenActivityByDialogIdTask.TryGetValue(dialog.Id, out var seenActivities) + ? seenActivities + : []; + + if (latestActivityByDialogIdTask.TryGetValue(dialog.Id, out var latestNonSeenActivity)) + { + activities.Add(latestNonSeenActivity); + } + + dialog.LatestActivities = _mapper.Map>(activities); + + foreach (var activity in dialog.LatestActivities + .Where(x => !string.IsNullOrWhiteSpace(x.SeenByEndUserIdHash))) + { + // Before we hash the end user id, check if the seen activity is for the current user + activity.SeenActivityIsCurrentEndUser = userPid == activity.SeenByEndUserIdHash; + // Hash end user ids + activity.SeenByEndUserIdHash = MappingUtils.HashPid(activity.SeenByEndUserIdHash, salt); + } + } } } diff --git a/src/Digdir.Domain.Dialogporten.Application/Features/V1/ServiceOwner/DialogActivities/Queries/Search/MappingProfile.cs b/src/Digdir.Domain.Dialogporten.Application/Features/V1/ServiceOwner/DialogActivities/Queries/Search/MappingProfile.cs index 969b4856c..0dfadecd8 100644 --- a/src/Digdir.Domain.Dialogporten.Application/Features/V1/ServiceOwner/DialogActivities/Queries/Search/MappingProfile.cs +++ b/src/Digdir.Domain.Dialogporten.Application/Features/V1/ServiceOwner/DialogActivities/Queries/Search/MappingProfile.cs @@ -11,4 +11,4 @@ public MappingProfile() .ForMember(dest => dest.Type, opt => opt.MapFrom(src => src.TypeId)) .ForMember(dest => dest.DeletedAt, opt => opt.MapFrom(src => src.Dialog.DeletedAt)); } -} \ No newline at end of file +} diff --git a/src/Digdir.Domain.Dialogporten.Application/Features/V1/ServiceOwner/Dialogs/Queries/Get/GetDialogDto.cs b/src/Digdir.Domain.Dialogporten.Application/Features/V1/ServiceOwner/Dialogs/Queries/Get/GetDialogDto.cs index 55ea0a823..3b5fa559c 100644 --- a/src/Digdir.Domain.Dialogporten.Application/Features/V1/ServiceOwner/Dialogs/Queries/Get/GetDialogDto.cs +++ b/src/Digdir.Domain.Dialogporten.Application/Features/V1/ServiceOwner/Dialogs/Queries/Get/GetDialogDto.cs @@ -54,7 +54,6 @@ public sealed class GetDialogDialogActivityDto public Guid Id { get; set; } public DateTimeOffset? CreatedAt { get; set; } public Uri? ExtendedType { get; set; } - public DialogActivityType.Values Type { get; set; } public Guid? RelatedActivityId { get; set; } diff --git a/src/Digdir.Domain.Dialogporten.Domain/Dialogs/Entities/Activities/DialogActivity.cs b/src/Digdir.Domain.Dialogporten.Domain/Dialogs/Entities/Activities/DialogActivity.cs index 23b75a50f..7f54f6a76 100644 --- a/src/Digdir.Domain.Dialogporten.Domain/Dialogs/Entities/Activities/DialogActivity.cs +++ b/src/Digdir.Domain.Dialogporten.Domain/Dialogs/Entities/Activities/DialogActivity.cs @@ -11,8 +11,8 @@ public class DialogActivity : IImmutableEntity, IAggregateCreatedHandler, IEvent { public Guid Id { get; set; } public DateTimeOffset CreatedAt { get; set; } - public Uri? ExtendedType { get; set; } + public string? SeenByEndUserId { get; set; } // === Dependent relationships === public DialogActivityType.Values TypeId { get; set; } diff --git a/src/Digdir.Domain.Dialogporten.Domain/Dialogs/Entities/DialogEntity.cs b/src/Digdir.Domain.Dialogporten.Domain/Dialogs/Entities/DialogEntity.cs index e4adaebe7..05c417472 100644 --- a/src/Digdir.Domain.Dialogporten.Domain/Dialogs/Entities/DialogEntity.cs +++ b/src/Digdir.Domain.Dialogporten.Domain/Dialogs/Entities/DialogEntity.cs @@ -3,6 +3,7 @@ using Digdir.Domain.Dialogporten.Domain.Dialogs.Entities.Content; using Digdir.Domain.Dialogporten.Domain.Dialogs.Entities.Elements; using Digdir.Domain.Dialogporten.Domain.Dialogs.Events; +using Digdir.Domain.Dialogporten.Domain.Localizations; using Digdir.Library.Entity.Abstractions; using Digdir.Library.Entity.Abstractions.Features.Aggregate; using Digdir.Library.Entity.Abstractions.Features.EventPublisher; @@ -33,6 +34,7 @@ public class DialogEntity : public DateTimeOffset? VisibleFrom { get; set; } public DateTimeOffset? DueAt { get; set; } public DateTimeOffset? ExpiresAt { get; set; } + // TODO: Remove, https://github.com/digdir/dialogporten/issues/398 public DateTimeOffset? ReadAt { get; set; } // === Dependent relationships === @@ -41,21 +43,21 @@ public class DialogEntity : // === Principal relationships === [AggregateChild] - public List Content { get; set; } = new(); + public List Content { get; set; } = []; [AggregateChild] - public List SearchTags { get; set; } = new(); + public List SearchTags { get; set; } = []; [AggregateChild] - public List Elements { get; set; } = new(); + public List Elements { get; set; } = []; [AggregateChild] - public List GuiActions { get; set; } = new(); + public List GuiActions { get; set; } = []; [AggregateChild] - public List ApiActions { get; set; } = new(); + public List ApiActions { get; set; } = []; [AggregateChild] - public List Activities { get; set; } = new(); + public List Activities { get; set; } = []; public void SoftDelete() { @@ -89,17 +91,42 @@ public void OnDelete(AggregateNode self, DateTimeOffset utcNow) _domainEvents.Add(new DialogDeletedDomainEvent(Id, ServiceResource, Party)); } - public void UpdateReadAt(DateTimeOffset timestamp) + public void UpdateSeenAt(string seenByEndUserId, string? seenByEndUserName) { - if ((ReadAt ?? DateTimeOffset.MinValue) >= UpdatedAt) + var lastSeenByAt = Activities + .Where(x => x.SeenByEndUserId == seenByEndUserId) + .MaxBy(x => x.CreatedAt) + ?.CreatedAt; + + if ((lastSeenByAt ?? DateTimeOffset.MinValue) >= UpdatedAt) { return; } - ReadAt = timestamp; - _domainEvents.Add(new DialogReadDomainEvent(Id)); + var performedBy = seenByEndUserName is not null + ? new DialogActivityPerformedBy + { + Localizations = + [ + new Localization + { + CultureCode = "nb-no", + Value = seenByEndUserName + } + ] + } + : null; + + Activities.Add(new DialogActivity + { + PerformedBy = performedBy, + SeenByEndUserId = seenByEndUserId, + TypeId = DialogActivityType.Values.Seen + }); + + _domainEvents.Add(new DialogSeenDomainEvent(Id)); } - private readonly List _domainEvents = new(); + private readonly List _domainEvents = []; public IReadOnlyCollection DomainEvents => _domainEvents; } diff --git a/src/Digdir.Domain.Dialogporten.Domain/Dialogs/Events/DialogReadDomainEvent.cs b/src/Digdir.Domain.Dialogporten.Domain/Dialogs/Events/DialogSeenDomainEvent.cs similarity index 60% rename from src/Digdir.Domain.Dialogporten.Domain/Dialogs/Events/DialogReadDomainEvent.cs rename to src/Digdir.Domain.Dialogporten.Domain/Dialogs/Events/DialogSeenDomainEvent.cs index fdf446a24..1a09da28a 100644 --- a/src/Digdir.Domain.Dialogporten.Domain/Dialogs/Events/DialogReadDomainEvent.cs +++ b/src/Digdir.Domain.Dialogporten.Domain/Dialogs/Events/DialogSeenDomainEvent.cs @@ -2,4 +2,4 @@ namespace Digdir.Domain.Dialogporten.Domain.Dialogs.Events; -public sealed record DialogReadDomainEvent(Guid DialogId) : DomainEvent; \ No newline at end of file +public sealed record DialogSeenDomainEvent(Guid DialogId) : DomainEvent; diff --git a/src/Digdir.Domain.Dialogporten.Infrastructure/Altinn/OrganizationRegistry/NameRegistryClient.cs b/src/Digdir.Domain.Dialogporten.Infrastructure/Altinn/OrganizationRegistry/NameRegistryClient.cs new file mode 100644 index 000000000..e90377dba --- /dev/null +++ b/src/Digdir.Domain.Dialogporten.Infrastructure/Altinn/OrganizationRegistry/NameRegistryClient.cs @@ -0,0 +1,28 @@ +using System.Globalization; +using Bogus; +using Digdir.Domain.Dialogporten.Application.Externals; +using Microsoft.Extensions.Caching.Distributed; + +namespace Digdir.Domain.Dialogporten.Infrastructure.Altinn.OrganizationRegistry; + +internal class NameRegistryClient : INameRegistry +{ + private readonly IDistributedCache _cache; + private readonly HttpClient _client; + + public NameRegistryClient(HttpClient client, IDistributedCache cache) + { + _client = client ?? throw new ArgumentNullException(nameof(client)); + _cache = cache ?? throw new ArgumentNullException(nameof(cache)); + } + + public async Task GetName(string personalIdentificationNumber, CancellationToken cancellationToken) + { + // TODO: Implement fetching from Altinn + // https://github.com/digdir/dialogporten/issues/321 + Randomizer.Seed = new Random((int)long.Parse(personalIdentificationNumber, new NumberFormatInfo())); + var name = new Faker().Name.FullName(); + + return await Task.FromResult(name); + } +} diff --git a/src/Digdir.Domain.Dialogporten.Infrastructure/Digdir.Domain.Dialogporten.Infrastructure.csproj b/src/Digdir.Domain.Dialogporten.Infrastructure/Digdir.Domain.Dialogporten.Infrastructure.csproj index d884d2c6a..f8f56065c 100644 --- a/src/Digdir.Domain.Dialogporten.Infrastructure/Digdir.Domain.Dialogporten.Infrastructure.csproj +++ b/src/Digdir.Domain.Dialogporten.Infrastructure/Digdir.Domain.Dialogporten.Infrastructure.csproj @@ -10,6 +10,7 @@ + diff --git a/src/Digdir.Domain.Dialogporten.Infrastructure/InfrastructureExtensions.cs b/src/Digdir.Domain.Dialogporten.Infrastructure/InfrastructureExtensions.cs index bcefe400d..ecb14febc 100644 --- a/src/Digdir.Domain.Dialogporten.Infrastructure/InfrastructureExtensions.cs +++ b/src/Digdir.Domain.Dialogporten.Infrastructure/InfrastructureExtensions.cs @@ -92,6 +92,11 @@ public static IServiceCollection AddInfrastructure(this IServiceCollection servi client.BaseAddress = services.GetRequiredService>().Value.AltinnCdn.BaseUri) .AddPolicyHandlerFromRegistry(PollyPolicy.DefaultHttpRetryPolicy); + services.AddHttpClient((services, client) => + // TODO: Correct base URL, https://github.com/digdir/dialogporten/issues/321 + client.BaseAddress = services.GetRequiredService>().Value.Altinn.BaseUri) + .AddPolicyHandlerFromRegistry(PollyPolicy.DefaultHttpRetryPolicy); + services.AddHttpClient((services, client) => { var altinnSettings = services.GetRequiredService>().Value.Altinn; diff --git a/src/Digdir.Domain.Dialogporten.Infrastructure/Persistence/Migrations/20240131101154_ChangeReadAtToSeenByPerUserOnDialog.Designer.cs b/src/Digdir.Domain.Dialogporten.Infrastructure/Persistence/Migrations/20240131101154_ChangeReadAtToSeenByPerUserOnDialog.Designer.cs new file mode 100644 index 000000000..a005cc6d6 --- /dev/null +++ b/src/Digdir.Domain.Dialogporten.Infrastructure/Persistence/Migrations/20240131101154_ChangeReadAtToSeenByPerUserOnDialog.Designer.cs @@ -0,0 +1,1208 @@ +// +using System; +using Digdir.Domain.Dialogporten.Infrastructure.Persistence; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Digdir.Domain.Dialogporten.Infrastructure.Persistence.Migrations +{ + [DbContext(typeof(DialogDbContext))] + [Migration("20240131101154_ChangeReadAtToSeenByPerUserOnDialog")] + partial class ChangeReadAtToSeenByPerUserOnDialog + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.1") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Digdir.Domain.Dialogporten.Domain.Dialogs.Entities.Actions.DialogApiAction", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("Action") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("AuthorizationAttribute") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("current_timestamp at time zone 'utc'"); + + b.Property("DialogElementId") + .HasColumnType("uuid"); + + b.Property("DialogId") + .HasColumnType("uuid"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("current_timestamp at time zone 'utc'"); + + b.HasKey("Id"); + + b.HasIndex("DialogElementId"); + + b.HasIndex("DialogId"); + + b.ToTable("DialogApiAction"); + }); + + modelBuilder.Entity("Digdir.Domain.Dialogporten.Domain.Dialogs.Entities.Actions.DialogApiActionEndpoint", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("ActionId") + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("current_timestamp at time zone 'utc'"); + + b.Property("Deprecated") + .HasColumnType("boolean"); + + b.Property("DocumentationUrl") + .HasMaxLength(1023) + .HasColumnType("character varying(1023)"); + + b.Property("HttpMethodId") + .HasColumnType("integer"); + + b.Property("RequestSchema") + .HasMaxLength(1023) + .HasColumnType("character varying(1023)"); + + b.Property("ResponseSchema") + .HasMaxLength(1023) + .HasColumnType("character varying(1023)"); + + b.Property("SunsetAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("current_timestamp at time zone 'utc'"); + + b.Property("Url") + .IsRequired() + .HasMaxLength(1023) + .HasColumnType("character varying(1023)"); + + b.Property("Version") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.HasKey("Id"); + + b.HasIndex("ActionId"); + + b.HasIndex("HttpMethodId"); + + b.ToTable("DialogApiActionEndpoint"); + }); + + modelBuilder.Entity("Digdir.Domain.Dialogporten.Domain.Dialogs.Entities.Actions.DialogGuiAction", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("Action") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("AuthorizationAttribute") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("current_timestamp at time zone 'utc'"); + + b.Property("DialogId") + .HasColumnType("uuid"); + + b.Property("IsBackChannel") + .HasColumnType("boolean"); + + b.Property("IsDeleteAction") + .HasColumnType("boolean"); + + b.Property("PriorityId") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("current_timestamp at time zone 'utc'"); + + b.Property("Url") + .IsRequired() + .HasMaxLength(1023) + .HasColumnType("character varying(1023)"); + + b.HasKey("Id"); + + b.HasIndex("DialogId"); + + b.HasIndex("PriorityId"); + + b.ToTable("DialogGuiAction"); + }); + + modelBuilder.Entity("Digdir.Domain.Dialogporten.Domain.Dialogs.Entities.Actions.DialogGuiActionPriority", b => + { + b.Property("Id") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.HasKey("Id"); + + b.ToTable("DialogGuiActionPriority"); + + b.HasData( + new + { + Id = 1, + Name = "Primary" + }, + new + { + Id = 2, + Name = "Secondary" + }, + new + { + Id = 3, + Name = "Tertiary" + }); + }); + + modelBuilder.Entity("Digdir.Domain.Dialogporten.Domain.Dialogs.Entities.Activities.DialogActivity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("current_timestamp at time zone 'utc'"); + + b.Property("DialogElementId") + .HasColumnType("uuid"); + + b.Property("DialogId") + .HasColumnType("uuid"); + + b.Property("ExtendedType") + .HasMaxLength(1023) + .HasColumnType("character varying(1023)"); + + b.Property("RelatedActivityId") + .HasColumnType("uuid"); + + b.Property("SeenByEndUserId") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("TypeId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("DialogElementId"); + + b.HasIndex("DialogId"); + + b.HasIndex("RelatedActivityId"); + + b.HasIndex("TypeId"); + + b.ToTable("DialogActivity"); + }); + + modelBuilder.Entity("Digdir.Domain.Dialogporten.Domain.Dialogs.Entities.Activities.DialogActivityType", b => + { + b.Property("Id") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.HasKey("Id"); + + b.ToTable("DialogActivityType"); + + b.HasData( + new + { + Id = 1, + Name = "Submission" + }, + new + { + Id = 2, + Name = "Feedback" + }, + new + { + Id = 3, + Name = "Information" + }, + new + { + Id = 4, + Name = "Error" + }, + new + { + Id = 5, + Name = "Closed" + }, + new + { + Id = 6, + Name = "Seen" + }, + new + { + Id = 7, + Name = "Forwarded" + }); + }); + + modelBuilder.Entity("Digdir.Domain.Dialogporten.Domain.Dialogs.Entities.Content.DialogContent", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("current_timestamp at time zone 'utc'"); + + b.Property("DialogId") + .HasColumnType("uuid"); + + b.Property("TypeId") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("current_timestamp at time zone 'utc'"); + + b.HasKey("Id"); + + b.HasIndex("TypeId"); + + b.HasIndex("DialogId", "TypeId") + .IsUnique(); + + b.ToTable("DialogContent"); + }); + + modelBuilder.Entity("Digdir.Domain.Dialogporten.Domain.Dialogs.Entities.Content.DialogContentType", b => + { + b.Property("Id") + .HasColumnType("integer"); + + b.Property("MaxLength") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("OutputInList") + .HasColumnType("boolean"); + + b.Property("RenderAsHtml") + .HasColumnType("boolean"); + + b.Property("Required") + .HasColumnType("boolean"); + + b.HasKey("Id"); + + b.ToTable("DialogContentType"); + + b.HasData( + new + { + Id = 1, + MaxLength = 255, + Name = "Title", + OutputInList = true, + RenderAsHtml = false, + Required = true + }, + new + { + Id = 2, + MaxLength = 255, + Name = "SenderName", + OutputInList = true, + RenderAsHtml = false, + Required = false + }, + new + { + Id = 3, + MaxLength = 255, + Name = "Summary", + OutputInList = true, + RenderAsHtml = false, + Required = true + }, + new + { + Id = 4, + MaxLength = 1023, + Name = "AdditionalInfo", + OutputInList = false, + RenderAsHtml = true, + Required = false + }); + }); + + modelBuilder.Entity("Digdir.Domain.Dialogporten.Domain.Dialogs.Entities.DialogEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("current_timestamp at time zone 'utc'"); + + b.Property("Deleted") + .HasColumnType("boolean"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DueAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ExpiresAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ExtendedStatus") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("ExternalReference") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("Org") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("Party") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("Progress") + .HasColumnType("integer"); + + b.Property("ReadAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Revision") + .IsConcurrencyToken() + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("ServiceResource") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("StatusId") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("current_timestamp at time zone 'utc'"); + + b.Property("VisibleFrom") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("CreatedAt"); + + b.HasIndex("DueAt"); + + b.HasIndex("StatusId"); + + b.HasIndex("UpdatedAt"); + + b.ToTable("Dialog", (string)null); + }); + + modelBuilder.Entity("Digdir.Domain.Dialogporten.Domain.Dialogs.Entities.DialogSearchTag", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("current_timestamp at time zone 'utc'"); + + b.Property("DialogId") + .HasColumnType("uuid"); + + b.Property("Value") + .IsRequired() + .HasMaxLength(63) + .HasColumnType("character varying(63)"); + + b.HasKey("Id"); + + b.HasIndex("DialogId", "Value") + .IsUnique(); + + b.ToTable("DialogSearchTag"); + }); + + modelBuilder.Entity("Digdir.Domain.Dialogporten.Domain.Dialogs.Entities.DialogStatus", b => + { + b.Property("Id") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.HasKey("Id"); + + b.ToTable("DialogStatus"); + + b.HasData( + new + { + Id = 1, + Name = "New" + }, + new + { + Id = 2, + Name = "InProgress" + }, + new + { + Id = 3, + Name = "Waiting" + }, + new + { + Id = 4, + Name = "Signing" + }, + new + { + Id = 5, + Name = "Cancelled" + }, + new + { + Id = 6, + Name = "Completed" + }); + }); + + modelBuilder.Entity("Digdir.Domain.Dialogporten.Domain.Dialogs.Entities.Elements.DialogElement", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("AuthorizationAttribute") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("current_timestamp at time zone 'utc'"); + + b.Property("DialogId") + .HasColumnType("uuid"); + + b.Property("ExternalReference") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("RelatedDialogElementId") + .HasColumnType("uuid"); + + b.Property("Type") + .HasMaxLength(1023) + .HasColumnType("character varying(1023)"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("current_timestamp at time zone 'utc'"); + + b.HasKey("Id"); + + b.HasIndex("DialogId"); + + b.HasIndex("RelatedDialogElementId"); + + b.ToTable("DialogElement"); + }); + + modelBuilder.Entity("Digdir.Domain.Dialogporten.Domain.Dialogs.Entities.Elements.DialogElementUrl", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("ConsumerTypeId") + .HasColumnType("integer"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("current_timestamp at time zone 'utc'"); + + b.Property("DialogElementId") + .HasColumnType("uuid"); + + b.Property("MimeType") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("current_timestamp at time zone 'utc'"); + + b.Property("Url") + .IsRequired() + .HasMaxLength(1023) + .HasColumnType("character varying(1023)"); + + b.HasKey("Id"); + + b.HasIndex("ConsumerTypeId"); + + b.HasIndex("DialogElementId"); + + b.ToTable("DialogElementUrl"); + }); + + modelBuilder.Entity("Digdir.Domain.Dialogporten.Domain.Dialogs.Entities.Elements.DialogElementUrlConsumerType", b => + { + b.Property("Id") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.HasKey("Id"); + + b.ToTable("DialogElementUrlConsumerType"); + + b.HasData( + new + { + Id = 1, + Name = "Gui" + }, + new + { + Id = 2, + Name = "Api" + }); + }); + + modelBuilder.Entity("Digdir.Domain.Dialogporten.Domain.Http.HttpVerb", b => + { + b.Property("Id") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.HasKey("Id"); + + b.ToTable("HttpVerb"); + + b.HasData( + new + { + Id = 1, + Name = "GET" + }, + new + { + Id = 2, + Name = "POST" + }, + new + { + Id = 3, + Name = "PUT" + }, + new + { + Id = 4, + Name = "PATCH" + }, + new + { + Id = 5, + Name = "DELETE" + }, + new + { + Id = 6, + Name = "HEAD" + }, + new + { + Id = 7, + Name = "OPTIONS" + }, + new + { + Id = 8, + Name = "TRACE" + }, + new + { + Id = 9, + Name = "CONNECT" + }); + }); + + modelBuilder.Entity("Digdir.Domain.Dialogporten.Domain.Localizations.Localization", b => + { + b.Property("LocalizationSetId") + .HasColumnType("uuid"); + + b.Property("CultureCode") + .HasMaxLength(15) + .HasColumnType("character varying(15)"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("current_timestamp at time zone 'utc'"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("current_timestamp at time zone 'utc'"); + + b.Property("Value") + .IsRequired() + .HasMaxLength(4095) + .HasColumnType("character varying(4095)"); + + b.HasKey("LocalizationSetId", "CultureCode"); + + b.ToTable("Localization"); + }); + + modelBuilder.Entity("Digdir.Domain.Dialogporten.Domain.Localizations.LocalizationSet", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("current_timestamp at time zone 'utc'"); + + b.Property("Discriminator") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.HasKey("Id"); + + b.ToTable("LocalizationSet"); + + b.HasDiscriminator("Discriminator").HasValue("LocalizationSet"); + + b.UseTphMappingStrategy(); + }); + + modelBuilder.Entity("Digdir.Domain.Dialogporten.Domain.Outboxes.OutboxMessage", b => + { + b.Property("EventId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("EventPayload") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("EventType") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.HasKey("EventId"); + + b.ToTable("OutboxMessage"); + }); + + modelBuilder.Entity("Digdir.Domain.Dialogporten.Domain.Outboxes.OutboxMessageConsumer", b => + { + b.Property("EventId") + .HasColumnType("uuid"); + + b.Property("ConsumerName") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.HasKey("EventId", "ConsumerName"); + + b.ToTable("OutboxMessageConsumer"); + }); + + modelBuilder.Entity("Digdir.Domain.Dialogporten.Domain.Dialogs.Entities.Actions.DialogGuiActionTitle", b => + { + b.HasBaseType("Digdir.Domain.Dialogporten.Domain.Localizations.LocalizationSet"); + + b.Property("GuiActionId") + .HasColumnType("uuid"); + + b.HasIndex("GuiActionId") + .IsUnique(); + + b.HasDiscriminator().HasValue("DialogGuiActionTitle"); + }); + + modelBuilder.Entity("Digdir.Domain.Dialogporten.Domain.Dialogs.Entities.Activities.DialogActivityDescription", b => + { + b.HasBaseType("Digdir.Domain.Dialogporten.Domain.Localizations.LocalizationSet"); + + b.Property("ActivityId") + .HasColumnType("uuid"); + + b.HasIndex("ActivityId") + .IsUnique(); + + b.ToTable("LocalizationSet", t => + { + t.Property("ActivityId") + .HasColumnName("DialogActivityDescription_ActivityId"); + }); + + b.HasDiscriminator().HasValue("DialogActivityDescription"); + }); + + modelBuilder.Entity("Digdir.Domain.Dialogporten.Domain.Dialogs.Entities.Activities.DialogActivityPerformedBy", b => + { + b.HasBaseType("Digdir.Domain.Dialogporten.Domain.Localizations.LocalizationSet"); + + b.Property("ActivityId") + .HasColumnType("uuid"); + + b.HasIndex("ActivityId") + .IsUnique(); + + b.HasDiscriminator().HasValue("DialogActivityPerformedBy"); + }); + + modelBuilder.Entity("Digdir.Domain.Dialogporten.Domain.Dialogs.Entities.Content.DialogContentValue", b => + { + b.HasBaseType("Digdir.Domain.Dialogporten.Domain.Localizations.LocalizationSet"); + + b.Property("DialogContentId") + .HasColumnType("uuid"); + + b.HasIndex("DialogContentId") + .IsUnique(); + + b.HasDiscriminator().HasValue("DialogContentValue"); + }); + + modelBuilder.Entity("Digdir.Domain.Dialogporten.Domain.Dialogs.Entities.Elements.DialogElementDisplayName", b => + { + b.HasBaseType("Digdir.Domain.Dialogporten.Domain.Localizations.LocalizationSet"); + + b.Property("ElementId") + .HasColumnType("uuid"); + + b.HasIndex("ElementId") + .IsUnique(); + + b.HasDiscriminator().HasValue("DialogElementDisplayName"); + }); + + modelBuilder.Entity("Digdir.Domain.Dialogporten.Domain.Dialogs.Entities.Actions.DialogApiAction", b => + { + b.HasOne("Digdir.Domain.Dialogporten.Domain.Dialogs.Entities.Elements.DialogElement", "DialogElement") + .WithMany("ApiActions") + .HasForeignKey("DialogElementId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("Digdir.Domain.Dialogporten.Domain.Dialogs.Entities.DialogEntity", "Dialog") + .WithMany("ApiActions") + .HasForeignKey("DialogId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Dialog"); + + b.Navigation("DialogElement"); + }); + + modelBuilder.Entity("Digdir.Domain.Dialogporten.Domain.Dialogs.Entities.Actions.DialogApiActionEndpoint", b => + { + b.HasOne("Digdir.Domain.Dialogporten.Domain.Dialogs.Entities.Actions.DialogApiAction", "Action") + .WithMany("Endpoints") + .HasForeignKey("ActionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Digdir.Domain.Dialogporten.Domain.Http.HttpVerb", "HttpMethod") + .WithMany() + .HasForeignKey("HttpMethodId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Action"); + + b.Navigation("HttpMethod"); + }); + + modelBuilder.Entity("Digdir.Domain.Dialogporten.Domain.Dialogs.Entities.Actions.DialogGuiAction", b => + { + b.HasOne("Digdir.Domain.Dialogporten.Domain.Dialogs.Entities.DialogEntity", "Dialog") + .WithMany("GuiActions") + .HasForeignKey("DialogId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Digdir.Domain.Dialogporten.Domain.Dialogs.Entities.Actions.DialogGuiActionPriority", "Priority") + .WithMany() + .HasForeignKey("PriorityId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Dialog"); + + b.Navigation("Priority"); + }); + + modelBuilder.Entity("Digdir.Domain.Dialogporten.Domain.Dialogs.Entities.Activities.DialogActivity", b => + { + b.HasOne("Digdir.Domain.Dialogporten.Domain.Dialogs.Entities.Elements.DialogElement", "DialogElement") + .WithMany("Activities") + .HasForeignKey("DialogElementId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Digdir.Domain.Dialogporten.Domain.Dialogs.Entities.DialogEntity", "Dialog") + .WithMany("Activities") + .HasForeignKey("DialogId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Digdir.Domain.Dialogporten.Domain.Dialogs.Entities.Activities.DialogActivity", "RelatedActivity") + .WithMany("RelatedActivities") + .HasForeignKey("RelatedActivityId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Digdir.Domain.Dialogporten.Domain.Dialogs.Entities.Activities.DialogActivityType", "Type") + .WithMany() + .HasForeignKey("TypeId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Dialog"); + + b.Navigation("DialogElement"); + + b.Navigation("RelatedActivity"); + + b.Navigation("Type"); + }); + + modelBuilder.Entity("Digdir.Domain.Dialogporten.Domain.Dialogs.Entities.Content.DialogContent", b => + { + b.HasOne("Digdir.Domain.Dialogporten.Domain.Dialogs.Entities.DialogEntity", "Dialog") + .WithMany("Content") + .HasForeignKey("DialogId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Digdir.Domain.Dialogporten.Domain.Dialogs.Entities.Content.DialogContentType", "Type") + .WithMany() + .HasForeignKey("TypeId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Dialog"); + + b.Navigation("Type"); + }); + + modelBuilder.Entity("Digdir.Domain.Dialogporten.Domain.Dialogs.Entities.DialogEntity", b => + { + b.HasOne("Digdir.Domain.Dialogporten.Domain.Dialogs.Entities.DialogStatus", "Status") + .WithMany() + .HasForeignKey("StatusId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Status"); + }); + + modelBuilder.Entity("Digdir.Domain.Dialogporten.Domain.Dialogs.Entities.DialogSearchTag", b => + { + b.HasOne("Digdir.Domain.Dialogporten.Domain.Dialogs.Entities.DialogEntity", "Dialog") + .WithMany("SearchTags") + .HasForeignKey("DialogId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Dialog"); + }); + + modelBuilder.Entity("Digdir.Domain.Dialogporten.Domain.Dialogs.Entities.Elements.DialogElement", b => + { + b.HasOne("Digdir.Domain.Dialogporten.Domain.Dialogs.Entities.DialogEntity", "Dialog") + .WithMany("Elements") + .HasForeignKey("DialogId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Digdir.Domain.Dialogporten.Domain.Dialogs.Entities.Elements.DialogElement", "RelatedDialogElement") + .WithMany("RelatedDialogElements") + .HasForeignKey("RelatedDialogElementId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Dialog"); + + b.Navigation("RelatedDialogElement"); + }); + + modelBuilder.Entity("Digdir.Domain.Dialogporten.Domain.Dialogs.Entities.Elements.DialogElementUrl", b => + { + b.HasOne("Digdir.Domain.Dialogporten.Domain.Dialogs.Entities.Elements.DialogElementUrlConsumerType", "ConsumerType") + .WithMany() + .HasForeignKey("ConsumerTypeId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Digdir.Domain.Dialogporten.Domain.Dialogs.Entities.Elements.DialogElement", "DialogElement") + .WithMany("Urls") + .HasForeignKey("DialogElementId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ConsumerType"); + + b.Navigation("DialogElement"); + }); + + modelBuilder.Entity("Digdir.Domain.Dialogporten.Domain.Localizations.Localization", b => + { + b.HasOne("Digdir.Domain.Dialogporten.Domain.Localizations.LocalizationSet", "LocalizationSet") + .WithMany("Localizations") + .HasForeignKey("LocalizationSetId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("LocalizationSet"); + }); + + modelBuilder.Entity("Digdir.Domain.Dialogporten.Domain.Outboxes.OutboxMessageConsumer", b => + { + b.HasOne("Digdir.Domain.Dialogporten.Domain.Outboxes.OutboxMessage", "OutboxMessage") + .WithMany("OutboxMessageConsumers") + .HasForeignKey("EventId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("OutboxMessage"); + }); + + modelBuilder.Entity("Digdir.Domain.Dialogporten.Domain.Dialogs.Entities.Actions.DialogGuiActionTitle", b => + { + b.HasOne("Digdir.Domain.Dialogporten.Domain.Dialogs.Entities.Actions.DialogGuiAction", "GuiAction") + .WithOne("Title") + .HasForeignKey("Digdir.Domain.Dialogporten.Domain.Dialogs.Entities.Actions.DialogGuiActionTitle", "GuiActionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("GuiAction"); + }); + + modelBuilder.Entity("Digdir.Domain.Dialogporten.Domain.Dialogs.Entities.Activities.DialogActivityDescription", b => + { + b.HasOne("Digdir.Domain.Dialogporten.Domain.Dialogs.Entities.Activities.DialogActivity", "Activity") + .WithOne("Description") + .HasForeignKey("Digdir.Domain.Dialogporten.Domain.Dialogs.Entities.Activities.DialogActivityDescription", "ActivityId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Activity"); + }); + + modelBuilder.Entity("Digdir.Domain.Dialogporten.Domain.Dialogs.Entities.Activities.DialogActivityPerformedBy", b => + { + b.HasOne("Digdir.Domain.Dialogporten.Domain.Dialogs.Entities.Activities.DialogActivity", "Activity") + .WithOne("PerformedBy") + .HasForeignKey("Digdir.Domain.Dialogporten.Domain.Dialogs.Entities.Activities.DialogActivityPerformedBy", "ActivityId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Activity"); + }); + + modelBuilder.Entity("Digdir.Domain.Dialogporten.Domain.Dialogs.Entities.Content.DialogContentValue", b => + { + b.HasOne("Digdir.Domain.Dialogporten.Domain.Dialogs.Entities.Content.DialogContent", "DialogContent") + .WithOne("Value") + .HasForeignKey("Digdir.Domain.Dialogporten.Domain.Dialogs.Entities.Content.DialogContentValue", "DialogContentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("DialogContent"); + }); + + modelBuilder.Entity("Digdir.Domain.Dialogporten.Domain.Dialogs.Entities.Elements.DialogElementDisplayName", b => + { + b.HasOne("Digdir.Domain.Dialogporten.Domain.Dialogs.Entities.Elements.DialogElement", "Element") + .WithOne("DisplayName") + .HasForeignKey("Digdir.Domain.Dialogporten.Domain.Dialogs.Entities.Elements.DialogElementDisplayName", "ElementId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Element"); + }); + + modelBuilder.Entity("Digdir.Domain.Dialogporten.Domain.Dialogs.Entities.Actions.DialogApiAction", b => + { + b.Navigation("Endpoints"); + }); + + modelBuilder.Entity("Digdir.Domain.Dialogporten.Domain.Dialogs.Entities.Actions.DialogGuiAction", b => + { + b.Navigation("Title"); + }); + + modelBuilder.Entity("Digdir.Domain.Dialogporten.Domain.Dialogs.Entities.Activities.DialogActivity", b => + { + b.Navigation("Description"); + + b.Navigation("PerformedBy"); + + b.Navigation("RelatedActivities"); + }); + + modelBuilder.Entity("Digdir.Domain.Dialogporten.Domain.Dialogs.Entities.Content.DialogContent", b => + { + b.Navigation("Value") + .IsRequired(); + }); + + modelBuilder.Entity("Digdir.Domain.Dialogporten.Domain.Dialogs.Entities.DialogEntity", b => + { + b.Navigation("Activities"); + + b.Navigation("ApiActions"); + + b.Navigation("Content"); + + b.Navigation("Elements"); + + b.Navigation("GuiActions"); + + b.Navigation("SearchTags"); + }); + + modelBuilder.Entity("Digdir.Domain.Dialogporten.Domain.Dialogs.Entities.Elements.DialogElement", b => + { + b.Navigation("Activities"); + + b.Navigation("ApiActions"); + + b.Navigation("DisplayName"); + + b.Navigation("RelatedDialogElements"); + + b.Navigation("Urls"); + }); + + modelBuilder.Entity("Digdir.Domain.Dialogporten.Domain.Localizations.LocalizationSet", b => + { + b.Navigation("Localizations"); + }); + + modelBuilder.Entity("Digdir.Domain.Dialogporten.Domain.Outboxes.OutboxMessage", b => + { + b.Navigation("OutboxMessageConsumers"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Digdir.Domain.Dialogporten.Infrastructure/Persistence/Migrations/20240131101154_ChangeReadAtToSeenByPerUserOnDialog.cs b/src/Digdir.Domain.Dialogporten.Infrastructure/Persistence/Migrations/20240131101154_ChangeReadAtToSeenByPerUserOnDialog.cs new file mode 100644 index 000000000..fd47b6c05 --- /dev/null +++ b/src/Digdir.Domain.Dialogporten.Infrastructure/Persistence/Migrations/20240131101154_ChangeReadAtToSeenByPerUserOnDialog.cs @@ -0,0 +1,29 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Digdir.Domain.Dialogporten.Infrastructure.Persistence.Migrations +{ + /// + public partial class ChangeReadAtToSeenByPerUserOnDialog : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "SeenByEndUserId", + table: "DialogActivity", + type: "character varying(255)", + maxLength: 255, + nullable: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "SeenByEndUserId", + table: "DialogActivity"); + } + } +} diff --git a/src/Digdir.Domain.Dialogporten.Infrastructure/Persistence/Migrations/DialogDbContextModelSnapshot.cs b/src/Digdir.Domain.Dialogporten.Infrastructure/Persistence/Migrations/DialogDbContextModelSnapshot.cs index 8bfea1073..301b04a95 100644 --- a/src/Digdir.Domain.Dialogporten.Infrastructure/Persistence/Migrations/DialogDbContextModelSnapshot.cs +++ b/src/Digdir.Domain.Dialogporten.Infrastructure/Persistence/Migrations/DialogDbContextModelSnapshot.cs @@ -17,7 +17,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) { #pragma warning disable 612, 618 modelBuilder - .HasAnnotation("ProductVersion", "8.0.0") + .HasAnnotation("ProductVersion", "8.0.1") .HasAnnotation("Relational:MaxIdentifierLength", 63); NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); @@ -231,6 +231,10 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("RelatedActivityId") .HasColumnType("uuid"); + b.Property("SeenByEndUserId") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + b.Property("TypeId") .HasColumnType("integer"); diff --git a/src/Digdir.Domain.Dialogporten.WebApi/Common/LocalDevelopmentUser.cs b/src/Digdir.Domain.Dialogporten.WebApi/Common/LocalDevelopmentUser.cs index 214415d69..c4107c5fd 100644 --- a/src/Digdir.Domain.Dialogporten.WebApi/Common/LocalDevelopmentUser.cs +++ b/src/Digdir.Domain.Dialogporten.WebApi/Common/LocalDevelopmentUser.cs @@ -10,6 +10,7 @@ internal sealed class LocalDevelopmentUser : IUser { new Claim(ClaimTypes.Name, "Local Development User"), new Claim(ClaimTypes.NameIdentifier, "local-development-user"), + new Claim("pid", "03886595947"), new Claim("scope", string.Join(" ", AuthorizationScope.AllScopes.Value)), new Claim("consumer", """ diff --git a/src/Digdir.Domain.Dialogporten.WebApi/Endpoints/V1/EndUser/Dialogs/GetDialogEndpoint.cs b/src/Digdir.Domain.Dialogporten.WebApi/Endpoints/V1/EndUser/Dialogs/GetDialogEndpoint.cs index 18c835d15..9d7ca8a88 100644 --- a/src/Digdir.Domain.Dialogporten.WebApi/Endpoints/V1/EndUser/Dialogs/GetDialogEndpoint.cs +++ b/src/Digdir.Domain.Dialogporten.WebApi/Endpoints/V1/EndUser/Dialogs/GetDialogEndpoint.cs @@ -40,7 +40,8 @@ await result.Match( return SendOkAsync(dto, ct); }, notFound => this.NotFoundAsync(notFound, ct), - deleted => this.GoneAsync(deleted, ct)); + deleted => this.GoneAsync(deleted, ct), + forbidden => this.ForbiddenAsync(forbidden, ct)); } } diff --git a/src/Digdir.Domain.Dialogporten.WebApi/Endpoints/V1/EndUser/Dialogs/SearchDialogEndpoint.cs b/src/Digdir.Domain.Dialogporten.WebApi/Endpoints/V1/EndUser/Dialogs/SearchDialogEndpoint.cs index c6affb3f9..3d29519c9 100644 --- a/src/Digdir.Domain.Dialogporten.WebApi/Endpoints/V1/EndUser/Dialogs/SearchDialogEndpoint.cs +++ b/src/Digdir.Domain.Dialogporten.WebApi/Endpoints/V1/EndUser/Dialogs/SearchDialogEndpoint.cs @@ -34,7 +34,8 @@ public override async Task HandleAsync(SearchDialogQuery req, CancellationToken var result = await _sender.Send(req, ct); await result.Match( paginatedDto => SendOkAsync(paginatedDto, ct), - validationError => this.BadRequestAsync(validationError, ct)); + validationError => this.BadRequestAsync(validationError, ct), + forbidden => this.ForbiddenAsync(forbidden, ct)); } } public sealed class SearchDialogEndpointSummary : Summary