Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(breaking): Move front channel embeds to content #862

Merged
merged 16 commits into from
Jun 19, 2024
Merged
2 changes: 1 addition & 1 deletion docs/schema/V1/schema.verified.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ type AuthorizedParty {
type Content {
type: ContentType!
value: [Localization!]!
mediaType: String
}

type Dialog {
Expand Down Expand Up @@ -115,7 +116,6 @@ type Element {
type ElementUrl {
id: UUID!
url: URL!
mediaType: String
consumerType: ElementUrlConsumer!
}

Expand Down
45 changes: 24 additions & 21 deletions docs/schema/V1/swagger.verified.json
Original file line number Diff line number Diff line change
Expand Up @@ -229,7 +229,8 @@
"Value": "Some Title",
"CultureCode": "en-us"
}
]
],
"MediaType": null
},
{
"Type": 3,
Expand All @@ -238,7 +239,8 @@
"Value": "Some Summary",
"CultureCode": "en-us"
}
]
],
"MediaType": null
}
],
"SearchTags": [
Expand Down Expand Up @@ -266,7 +268,6 @@
{
"Id": "858177cb-8584-4d10-a086-3a5defa7a6c3",
"Url": "http://example.com/some-url",
"MediaType": "application/json",
"ConsumerType": 0
}
]
Expand Down Expand Up @@ -2301,6 +2302,10 @@
"items": {
"$ref": "#/components/schemas/LocalizationDto"
}
},
"mediaType": {
"type": "string",
"nullable": true
}
}
},
Expand All @@ -2312,14 +2317,16 @@
"SenderName",
"Summary",
"AdditionalInfo",
"ExtendedStatus"
"ExtendedStatus",
"MainContentReference"
],
"enum": [
"Title",
"SenderName",
"Summary",
"AdditionalInfo",
"ExtendedStatus"
"ExtendedStatus",
"MainContentReference"
]
},
"LocalizationDto": {
Expand Down Expand Up @@ -2397,10 +2404,6 @@
"type": "string",
"format": "uri"
},
"mediaType": {
"type": "string",
"nullable": true
},
"consumerType": {
"$ref": "#/components/schemas/DialogElementUrlConsumerType_Values"
}
Expand Down Expand Up @@ -2902,6 +2905,10 @@
"items": {
"$ref": "#/components/schemas/LocalizationDto"
}
},
"mediaType": {
"type": "string",
"nullable": true
}
}
},
Expand Down Expand Up @@ -2962,10 +2969,6 @@
"type": "string",
"format": "uri"
},
"mediaType": {
"type": "string",
"nullable": true
},
"consumerType": {
"$ref": "#/components/schemas/DialogElementUrlConsumerType_Values"
}
Expand Down Expand Up @@ -3250,6 +3253,10 @@
"items": {
"$ref": "#/components/schemas/LocalizationDto"
}
},
"mediaType": {
"type": "string",
"nullable": true
}
}
},
Expand Down Expand Up @@ -3311,10 +3318,6 @@
"type": "string",
"format": "uri"
},
"mediaType": {
"type": "string",
"nullable": true
},
"consumerType": {
"$ref": "#/components/schemas/DialogElementUrlConsumerType_Values"
}
Expand Down Expand Up @@ -3947,6 +3950,10 @@
"items": {
"$ref": "#/components/schemas/LocalizationDto"
}
},
"mediaType": {
"type": "string",
"nullable": true
}
}
},
Expand Down Expand Up @@ -4005,10 +4012,6 @@
"type": "string",
"format": "uri"
},
"mediaType": {
"type": "string",
"nullable": true
},
"consumerType": {
"$ref": "#/components/schemas/DialogElementUrlConsumerType_Values"
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,25 +1,35 @@
using Digdir.Domain.Dialogporten.Application.Features.V1.Common.Localizations;
using FluentValidation;
using HtmlAgilityPack;
using Markdig;

namespace Digdir.Domain.Dialogporten.Application.Common.Extensions.FluentValidation;

internal static class FluentValidationLocalizationDtoExtensions
{
private static readonly string[] AllowedTags = ["p", "a", "br", "em", "strong", "ul", "ol", "li"];
private static readonly string ContainsValidHtmlError =
$"{{PropertyName}} contains unsupported html. The following tags are supported: " +
"Value contains unsupported HTML/markdown. The following tags are supported: " +
$"[{string.Join(",", AllowedTags.Select(x => '<' + x + '>'))}]. Tag attributes " +
$"are not supported except for on '<a>' which must contain a 'href' starting " +
$"with 'https://'.";
"are not supported except for on '<a>' which must contain a 'href' starting " +
"with 'https://'.";

public static IRuleBuilderOptions<T, LocalizationDto> ContainsValidHtml<T>(this IRuleBuilder<T, LocalizationDto> ruleBuilder)
public static IRuleBuilderOptions<T, LocalizationDto> ContainsValidHtml<T>(
this IRuleBuilder<T, LocalizationDto> ruleBuilder)
{
return ruleBuilder
.Must(x => x.Value is null || x.Value.HtmlAgilityPackCheck())
.WithMessage(ContainsValidHtmlError);
}

public static IRuleBuilderOptions<T, LocalizationDto> ContainsValidMarkdown<T>(
this IRuleBuilder<T, LocalizationDto> ruleBuilder)
{
return ruleBuilder
.Must(x => x.Value is null || Markdown.ToHtml(x.Value).HtmlAgilityPackCheck())
.WithMessage(ContainsValidHtmlError);
}

private static bool HtmlAgilityPackCheck(this string html)
{
var doc = new HtmlDocument();
Expand All @@ -33,8 +43,8 @@ private static bool HtmlAgilityPackCheck(this string html)
{
return false;
}
// If the node is a hyperlink, it should only have an href attribute
// and it should start with 'https://'
// If the node is a hyperlink, it should only have a href attribute,
// and it must start with 'https://'
if (node.IsAnchorTag())
{
if (!node.IsValidAnchorTag())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
<ItemGroup>
<PackageReference Include="AutoMapper" Version="13.0.1" />
<PackageReference Include="HtmlAgilityPack" Version="1.11.61" />
<PackageReference Include="Markdig.Signed" Version="0.37.0" />
<PackageReference Include="Microsoft.Extensions.Hosting" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="8.0.0" />
<PackageReference Include="FluentValidation.DependencyInjectionExtensions" Version="11.9.1" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ public sealed class GetDialogElementUrlDto
{
public Guid Id { get; set; }
public Uri Url { get; set; } = null!;
public string? MediaType { get; set; }

public DialogElementUrlConsumerType.Values ConsumerType { get; set; }
}
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ public sealed class GetDialogContentDto
{
public DialogContentType.Values Type { get; set; }
public List<LocalizationDto> Value { get; set; } = [];
public string? MediaType { get; set; }
}

public sealed class GetDialogDialogActivityDto
Expand Down Expand Up @@ -130,7 +131,6 @@ public sealed class GetDialogDialogElementUrlDto
{
public Guid Id { get; set; }
public Uri Url { get; set; } = null!;
public string? MediaType { get; set; }

public DialogElementUrlConsumerType.Values ConsumerType { get; set; }
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ public sealed class GetDialogElementUrlDto
{
public Guid Id { get; set; }
public Uri Url { get; set; } = null!;
public string? MediaType { get; set; }

public DialogElementUrlConsumerType.Values ConsumerType { get; set; }
}
Original file line number Diff line number Diff line change
Expand Up @@ -128,9 +128,27 @@ public CreateDialogContentDtoValidator()
ClassLevelCascadeMode = CascadeMode.Stop;
RuleFor(x => x.Type)
.IsInEnum();
RuleFor(x => x.MediaType)
.Must((dto, value) =>
{
var type = DialogContentType.GetValue(dto.Type);
return value is null ? type.AllowedMediaTypes == null :
type.AllowedMediaTypes != null && type.AllowedMediaTypes.Contains(value);
})
.WithMessage(x =>
$"{{PropertyName}} '{x.MediaType ?? "null"}' is not allowed for content type {DialogContentType.GetValue(x.Type).Name}. " +
$"Valid media types are: {(DialogContentType.GetValue(x.Type).AllowedMediaTypes == null ? "None" :
$"{string.Join(", ", DialogContentType.GetValue(x.Type).AllowedMediaTypes!)}")}");
RuleForEach(x => x.Value)
.ContainsValidHtml()
.When(x => DialogContentType.GetValue(x.Type).RenderAsHtml);
.When(x => x.MediaType is not null && (x.MediaType == MediaTypes.Html));
RuleForEach(x => x.Value)
.ContainsValidMarkdown()
.When(x => x.MediaType is not null && x.MediaType == MediaTypes.Markdown);
RuleForEach(x => x.Value)
.Must(x => Uri.TryCreate(x.Value, UriKind.Absolute, out var uri) && uri.Scheme == Uri.UriSchemeHttps)
.When(x => x.MediaType is not null && x.MediaType.StartsWith(MediaTypes.EmbeddablePrefix, StringComparison.InvariantCultureIgnoreCase))
.WithMessage("{PropertyName} must be a valid HTTPS URL for embeddable content types");
RuleFor(x => x.Value)
.NotEmpty()
.SetValidator(x => new LocalizationDtosValidator(DialogContentType.GetValue(x.Type).MaxLength));
Expand Down Expand Up @@ -171,13 +189,6 @@ public CreateDialogDialogElementUrlDtoValidator()
.NotNull()
.IsValidUri()
.MaximumLength(Constants.DefaultMaxUriLength);
RuleFor(x => x.MediaType)
.MaximumLength(Constants.DefaultMaxStringLength);
RuleFor(x => x.MediaType)
.Must(MediaTypes.IsValid!)
.When(x => x.MediaType != null)
.WithMessage("Invalid media type, see docs for complete list <URL TDB>");

RuleFor(x => x.ConsumerType)
.IsInEnum();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ public sealed class CreateDialogContentDto
{
public DialogContentType.Values Type { get; set; }
public List<LocalizationDto> Value { get; set; } = [];
public string? MediaType { get; set; }
}

public sealed class CreateDialogSearchTagDto
Expand Down Expand Up @@ -109,7 +110,6 @@ public sealed class CreateDialogDialogElementDto
public sealed class CreateDialogDialogElementUrlDto
{
public Uri Url { get; set; } = null!;
public string? MediaType { get; set; }

public DialogElementUrlConsumerType.Values ConsumerType { get; set; }
}
Original file line number Diff line number Diff line change
Expand Up @@ -123,9 +123,27 @@ public UpdateDialogContentDtoValidator()
ClassLevelCascadeMode = CascadeMode.Stop;
RuleFor(x => x.Type)
.IsInEnum();
RuleFor(x => x.MediaType)
.Must((dto, value) =>
{
var type = DialogContentType.GetValue(dto.Type);
return value is null ? type.AllowedMediaTypes == null :
type.AllowedMediaTypes != null && type.AllowedMediaTypes.Contains(value);
})
.WithMessage(x =>
$"{{PropertyName}} '{x.MediaType ?? "null"}' is not allowed for content type {DialogContentType.GetValue(x.Type).Name}. " +
$"Valid media types are: {(DialogContentType.GetValue(x.Type).AllowedMediaTypes == null ? "None" :
$"{string.Join(", ", DialogContentType.GetValue(x.Type).AllowedMediaTypes!)}")}");
RuleForEach(x => x.Value)
.ContainsValidHtml()
.When(x => DialogContentType.GetValue(x.Type).RenderAsHtml);
.When(x => x.MediaType is not null && (x.MediaType == MediaTypes.Html));
RuleForEach(x => x.Value)
.ContainsValidMarkdown()
.When(x => x.MediaType is not null && x.MediaType == MediaTypes.Markdown);
RuleForEach(x => x.Value)
.Must(x => Uri.TryCreate(x.Value, UriKind.Absolute, out var uri) && uri.Scheme == Uri.UriSchemeHttps)
.When(x => x.MediaType is not null && x.MediaType.StartsWith(MediaTypes.EmbeddablePrefix, StringComparison.InvariantCultureIgnoreCase))
.WithMessage("{PropertyName} must be a valid HTTPS URL for embeddable content types");
RuleFor(x => x.Value)
.NotEmpty()
.SetValidator(x => new LocalizationDtosValidator(DialogContentType.GetValue(x.Type).MaxLength));
Expand Down Expand Up @@ -168,12 +186,12 @@ public UpdateDialogDialogElementUrlDtoValidator()
.NotNull()
.IsValidUri()
.MaximumLength(Constants.DefaultMaxUriLength);
RuleFor(x => x.MediaType)
.MaximumLength(Constants.DefaultMaxStringLength);
RuleFor(x => x.MediaType)
.Must(MediaTypes.IsValid!)
.When(x => x.MediaType != null)
.WithMessage("Invalid media type, see docs for complete list <URL TDB>");
// RuleFor(x => x.MediaType)
// .MaximumLength(Constants.DefaultMaxStringLength);
// RuleFor(x => x.MediaType)
// .Must(MediaTypes.IsValid!)
// .When(x => x.MediaType != null)
// .WithMessage("Invalid media type, see docs for complete list <URL TDB>");
oskogstad marked this conversation as resolved.
Show resolved Hide resolved
RuleFor(x => x.ConsumerType)
.IsInEnum();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ public sealed class UpdateDialogContentDto
{
public DialogContentType.Values Type { get; set; }
public List<LocalizationDto> Value { get; set; } = [];
public string? MediaType { get; set; }
}

public sealed class UpdateDialogSearchTagDto
Expand Down Expand Up @@ -112,7 +113,6 @@ public sealed class UpdateDialogDialogElementUrlDto
{
public Guid? Id { get; set; }
public Uri Url { get; set; } = null!;
public string? MediaType { get; set; }

public DialogElementUrlConsumerType.Values ConsumerType { get; set; }
}
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ public sealed class GetDialogContentDto
{
public DialogContentType.Values Type { get; set; }
public List<LocalizationDto> Value { get; set; } = [];
public string? MediaType { get; set; }
}

public sealed class GetDialogSearchTagDto
Expand Down Expand Up @@ -131,7 +132,6 @@ public sealed class GetDialogDialogElementUrlDto
{
public Guid Id { get; set; }
public Uri Url { get; set; } = null!;
public string? MediaType { get; set; }

public DialogElementUrlConsumerType.Values ConsumerType { get; set; }
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ public class DialogContent : IEntity
public DateTimeOffset CreatedAt { get; set; }
public DateTimeOffset UpdatedAt { get; set; }

public string? MediaType { get; set; }

// === Dependent relationships ===
public Guid DialogId { get; set; }
public DialogEntity Dialog { get; set; } = null!;
Expand Down
Loading