Skip to content
This repository has been archived by the owner on Jul 9, 2024. It is now read-only.

Commit

Permalink
Implemented Portal seeding. (#20)
Browse files Browse the repository at this point in the history
* Implemented Portal seeding.

* Implemented retry.
  • Loading branch information
Utar94 authored May 2, 2024
1 parent e400830 commit f1a7e28
Show file tree
Hide file tree
Showing 28 changed files with 672 additions and 2 deletions.
13 changes: 11 additions & 2 deletions backend/Master.sln
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,13 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{01817770
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Logitar.Master.IntegrationTests", "tests\Logitar.Master.IntegrationTests\Logitar.Master.IntegrationTests.csproj", "{1ED47AD8-2738-47A2-A39C-422DB09953C7}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Logitar.Master.Application.UnitTests", "tests\Logitar.Master.Application.UnitTests\Logitar.Master.Application.UnitTests.csproj", "{A88235DD-8A9C-4E58-88DF-2C0ED0F96C60}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Logitar.Master.Application.UnitTests", "tests\Logitar.Master.Application.UnitTests\Logitar.Master.Application.UnitTests.csproj", "{A88235DD-8A9C-4E58-88DF-2C0ED0F96C60}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Logitar.Master.Tests", "tests\Logitar.Master.Tests\Logitar.Master.Tests.csproj", "{B83E66B3-FC15-4900-B966-85F44D47F784}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Logitar.Master.Tests", "tests\Logitar.Master.Tests\Logitar.Master.Tests.csproj", "{B83E66B3-FC15-4900-B966-85F44D47F784}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tools", "Tools", "{9B475D83-5980-4A48-BB50-65DE79FD56A0}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Logitar.Master.PortalSeeding.Worker", "tools\Logitar.Master.PortalSeeding.Worker\Logitar.Master.PortalSeeding.Worker.csproj", "{20D21B84-F5E4-4E01-ACE8-C5A8FB1AC154}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Expand Down Expand Up @@ -80,6 +84,10 @@ Global
{B83E66B3-FC15-4900-B966-85F44D47F784}.Debug|Any CPU.Build.0 = Debug|Any CPU
{B83E66B3-FC15-4900-B966-85F44D47F784}.Release|Any CPU.ActiveCfg = Release|Any CPU
{B83E66B3-FC15-4900-B966-85F44D47F784}.Release|Any CPU.Build.0 = Release|Any CPU
{20D21B84-F5E4-4E01-ACE8-C5A8FB1AC154}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{20D21B84-F5E4-4E01-ACE8-C5A8FB1AC154}.Debug|Any CPU.Build.0 = Debug|Any CPU
{20D21B84-F5E4-4E01-ACE8-C5A8FB1AC154}.Release|Any CPU.ActiveCfg = Release|Any CPU
{20D21B84-F5E4-4E01-ACE8-C5A8FB1AC154}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand All @@ -88,6 +96,7 @@ Global
{1ED47AD8-2738-47A2-A39C-422DB09953C7} = {01817770-861B-453F-A98F-E42840A5E16F}
{A88235DD-8A9C-4E58-88DF-2C0ED0F96C60} = {01817770-861B-453F-A98F-E42840A5E16F}
{B83E66B3-FC15-4900-B966-85F44D47F784} = {01817770-861B-453F-A98F-E42840A5E16F}
{20D21B84-F5E4-4E01-ACE8-C5A8FB1AC154} = {9B475D83-5980-4A48-BB50-65DE79FD56A0}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {2EF5DF67-96A3-4877-949F-E046443F9E9A}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
using MediatR;

namespace Logitar.Master.PortalSeeding.Worker.Commands;

internal record SeedDictionariesCommand : INotification;
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
using Logitar.Portal.Contracts;
using Logitar.Portal.Contracts.Dictionaries;
using Logitar.Portal.Contracts.Search;
using MediatR;

namespace Logitar.Master.PortalSeeding.Worker.Commands;

internal class SeedDictionariesCommandHandler : INotificationHandler<SeedDictionariesCommand>
{
private readonly IDictionaryClient _dictionaries;
private readonly ILogger<SeedDictionariesCommandHandler> _logger;

public SeedDictionariesCommandHandler(IDictionaryClient dictionaries, ILogger<SeedDictionariesCommandHandler> logger)
{
_dictionaries = dictionaries;
_logger = logger;
}

public async Task Handle(SeedDictionariesCommand _, CancellationToken cancellationToken)
{
RequestContext context = new(cancellationToken);

SearchResults<Dictionary> results = await _dictionaries.SearchAsync(new SearchDictionariesPayload(), context);
Dictionary<string, Dictionary> dictionaries = new(capacity: results.Items.Count);
foreach (Dictionary dictionary in results.Items)
{
dictionaries[dictionary.Locale.Code] = dictionary;
}

string[] files = Directory.GetFiles("Dictionaries");
foreach (string path in files)
{
string locale = Path.GetFileNameWithoutExtension(path);
if (dictionaries.TryGetValue(locale, out Dictionary? dictionary))
{
_logger.LogInformation("The dictionary '{Locale}' already exists (Id={Id}).", dictionary.Locale.Code, dictionary.Id);
}
else
{
CreateDictionaryPayload payload = new(locale);
dictionary = await _dictionaries.CreateAsync(payload, context);
dictionaries[locale] = dictionary;

string json = await File.ReadAllTextAsync(path, Encoding.UTF8, cancellationToken);
Dictionary<string, string>? entries = JsonSerializer.Deserialize<Dictionary<string, string>>(json);
if (entries != null)
{
ReplaceDictionaryPayload replace = new(locale);
foreach (KeyValuePair<string, string> entry in entries)
{
replace.Entries.Add(new DictionaryEntry(entry));
}
dictionary = await _dictionaries.ReplaceAsync(dictionary.Id, replace, dictionary.Version, context)
?? throw new InvalidOperationException($"The dictionary 'Id={dictionary.Id}' replace result should not be null.");
}

_logger.LogInformation("The dictionary '{Locale}' has been created (Id={Id}).", dictionary.Locale.Code, dictionary.Id);
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
using MediatR;

namespace Logitar.Master.PortalSeeding.Worker.Commands;

internal record SeedRealmCommand : INotification;
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
using Logitar.Portal.Contracts;
using Logitar.Portal.Contracts.Realms;
using MediatR;

namespace Logitar.Master.PortalSeeding.Worker.Commands;

internal class SeedRealmCommandHandler : INotificationHandler<SeedRealmCommand>
{
private readonly ILogger<SeedRealmCommandHandler> _logger;
private readonly IRealmClient _realms;
private readonly string _uniqueSlug;

public SeedRealmCommandHandler(IConfiguration configuration, ILogger<SeedRealmCommandHandler> logger, IRealmClient realms)
{
_logger = logger;
_realms = realms;
_uniqueSlug = configuration.GetValue<string>("Portal:Realm") ?? throw new ArgumentException("The configuration 'Portal:Realm' is required.", nameof(configuration));
}

public async Task Handle(SeedRealmCommand _, CancellationToken cancellationToken)
{
RequestContext context = new(cancellationToken);

Realm? realm = await _realms.ReadAsync(id: null, _uniqueSlug, context);
if (realm == null)
{
CreateRealmPayload payload = new(_uniqueSlug, secret: string.Empty)
{
DisplayName = "Master",
Description = "This is the realm of the Master project management suite.",
DefaultLocale = "en",
Url = "http://localhost:7791"
};
realm = await _realms.CreateAsync(payload, context);
_logger.LogInformation("The realm '{UniqueSlug}' has been created (Id={Id}).", realm.UniqueSlug, realm.Id);
}
else
{
_logger.LogInformation("The realm '{UniqueSlug}' already exists (Id={Id}).", realm.UniqueSlug, realm.Id);
}

WorkerPortalSettings.Instance.SetRealm(realm);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
using MediatR;

namespace Logitar.Master.PortalSeeding.Worker.Commands;

internal record SeedSendersCommand : INotification;
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
using Logitar.Portal.Contracts;
using Logitar.Portal.Contracts.Search;
using Logitar.Portal.Contracts.Senders;
using MediatR;

namespace Logitar.Master.PortalSeeding.Worker.Commands;

internal class SeedSendersCommandHandler : INotificationHandler<SeedSendersCommand>
{
private readonly IConfiguration _configuration;
private readonly ILogger<SeedSendersCommandHandler> _logger;
private readonly ISenderClient _senders;

public SeedSendersCommandHandler(IConfiguration configuration, ILogger<SeedSendersCommandHandler> logger, ISenderClient senders)
{
_configuration = configuration;
_logger = logger;
_senders = senders;
}

public async Task Handle(SeedSendersCommand _, CancellationToken cancellationToken)
{
RequestContext context = new(cancellationToken);

SearchResults<Sender> results = await _senders.SearchAsync(new SearchSendersPayload(), context);
Dictionary<string, Sender> senders = new(capacity: results.Items.Count);
foreach (Sender sender in results.Items)
{
string? key = sender.GetKey();
if (key != null)
{
senders[key] = sender;
}
}

IEnumerable<CreateSenderPayload> payloads = _configuration.GetSection("Senders").GetChildren()
.Select(section => section.Get<CreateSenderPayload>() ?? new());
foreach (CreateSenderPayload payload in payloads)
{
string? key = payload.GetKey();
if (key != null)
{
string[] values = key.Split(':');
if (senders.TryGetValue(key, out Sender? sender))
{
_logger.LogInformation("The {ContactType} sender '{ContactValue}' already exists (Id={Id}).", values[0], values[1], sender.Id);
}
else
{
sender = await _senders.CreateAsync(payload, context);
senders[key] = sender;
_logger.LogInformation("The {ContactType} sender '{ContactValue}' has been created (Id={Id}).", values[0], values[1], sender.Id);
}
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
using MediatR;

namespace Logitar.Master.PortalSeeding.Worker.Commands;

internal record SeedTemplatesCommand : INotification;
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
using Logitar.Portal.Contracts;
using Logitar.Portal.Contracts.Search;
using Logitar.Portal.Contracts.Templates;
using MediatR;

namespace Logitar.Master.PortalSeeding.Worker.Commands;

internal class SeedTemplatesCommandHandler : INotificationHandler<SeedTemplatesCommand>
{
private static readonly Dictionary<string, string> _contentTypes = new()
{
[".html"] = MediaTypeNames.Text.Html,
[".txt"] = MediaTypeNames.Text.Plain
};

private readonly ILogger<SeedTemplatesCommandHandler> _logger;
private readonly ITemplateClient _templates;

public SeedTemplatesCommandHandler(ILogger<SeedTemplatesCommandHandler> logger, ITemplateClient templates)
{
_logger = logger;
_templates = templates;
}

public async Task Handle(SeedTemplatesCommand _, CancellationToken cancellationToken)
{
RequestContext context = new(cancellationToken);

SearchResults<Template> results = await _templates.SearchAsync(new SearchTemplatesPayload(), context);
Dictionary<string, Template> templates = new(capacity: results.Items.Count);
foreach (Template template in results.Items)
{
templates[template.UniqueKey] = template;
}

Dictionary<string, Content> contents = await LoadContentsAsync(cancellationToken);

string json = await File.ReadAllTextAsync("templates.json", Encoding.UTF8, cancellationToken);
IEnumerable<TemplateSummary>? summaries = JsonSerializer.Deserialize<IEnumerable<TemplateSummary>>(json);
if (summaries != null)
{
foreach (TemplateSummary summary in summaries)
{
if (templates.TryGetValue(summary.UniqueKey, out Template? template))
{
_logger.LogInformation("The template '{UniqueKey}' already exists (Id={Id}).", template.UniqueKey, template.Id);
}
else
{
string subject = $"{summary.UniqueKey}_Subject";
Content content = contents[summary.UniqueKey];
CreateTemplatePayload payload = new(summary.UniqueKey, subject, content)
{
DisplayName = summary.DisplayName,
Description = summary.Description
};
template = await _templates.CreateAsync(payload, context);
templates[summary.UniqueKey] = template;
_logger.LogInformation("The template '{UniqueKey}' has been created (Id={Id}).", template.UniqueKey, template.Id);
}
}
}
}

private static async Task<Dictionary<string, Content>> LoadContentsAsync(CancellationToken cancellationToken)
{
string[] files = Directory.GetFiles("Templates");
Dictionary<string, Content> contents = new(capacity: files.Length);

foreach (string path in files)
{
string name = Path.GetFileNameWithoutExtension(path);
string extension = Path.GetExtension(path);

string type = _contentTypes[extension];
string text = await File.ReadAllTextAsync(path, Encoding.UTF8, cancellationToken);

contents[name] = new(type, text);
}

return contents;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
namespace Logitar.Master.PortalSeeding.Worker;

public enum ContactType
{
Email = 0,
Phone = 1
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"AccountAuthentication_ClickLink": "If that someone is you, click the link below to access your account.",
"AccountAuthentication_Reason": "You received this message since someone tried to access your account.",
"AccountAuthentication_Subject": "Access your account",
"ContactVerificationPhone_Content": "Your one-time code has arrived: {OneTimePassword}. Do not disclose it to anyone.",
"ContactVerificationPhone_Subject": "Phone number verification",
"Cordially": "Cordially,",
"Hello": "Hello {name}!",
"MultiFactorAuthenticationEmail_Content": "Your one-time code has arrived. Do not disclose it to anyone.",
"MultiFactorAuthenticationEmail_Subject": "Access your account",
"MultiFactorAuthenticationPhone_Content": "Your one-time code has arrived: {OneTimePassword}. Do not disclose it to anyone.",
"MultiFactorAuthenticationPhone_Subject": "Access your account",
"Team": "The Logitar Team"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"AccountAuthentication_ClickLink": "S’il s’agit bien de vous, cliquez sur le lien ci-dessous afin d’accéder à votre compte.",
"AccountAuthentication_Reason": "Vous avez reçu ce courriel puisque quelqu’un a tenté d’accéder à votre compte.",
"AccountAuthentication_Subject": "Accédez à votre compte",
"ContactVerificationPhone_Content": "Votre code à usage unique est arrivé : {OneTimePassword}. Ne le divulguez à personne.",
"ContactVerificationPhone_Subject": "Vérification du numéro de téléphone",
"Cordially": "Cordialement,",
"Hello": "Bonjour {name} !",
"MultiFactorAuthenticationEmail_Content": "Votre code à usage unique est arrivé. Ne le divulguez à personne.",
"MultiFactorAuthenticationEmail_Subject": "Accédez à votre compte",
"MultiFactorAuthenticationPhone_Content": "Votre code à usage unique est arrivé : {OneTimePassword}. Ne le divulguez à personne.",
"MultiFactorAuthenticationPhone_Subject": "Accédez à votre compte",
"Team": "L’équipe Logitar"
}
23 changes: 23 additions & 0 deletions backend/tools/Logitar.Master.PortalSeeding.Worker/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
#See https://aka.ms/customizecontainer to learn how to customize your debug container and how Visual Studio uses this Dockerfile to build your images for faster debugging.

FROM mcr.microsoft.com/dotnet/runtime:8.0 AS base
USER app
WORKDIR /app

FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
ARG BUILD_CONFIGURATION=Release
WORKDIR /src
COPY ["tools/Logitar.Master.PortalSeeding.Worker/Logitar.Master.PortalSeeding.Worker.csproj", "tools/Logitar.Master.PortalSeeding.Worker/"]
RUN dotnet restore "./tools/Logitar.Master.PortalSeeding.Worker/Logitar.Master.PortalSeeding.Worker.csproj"
COPY . .
WORKDIR "/src/tools/Logitar.Master.PortalSeeding.Worker"
RUN dotnet build "./Logitar.Master.PortalSeeding.Worker.csproj" -c $BUILD_CONFIGURATION -o /app/build

FROM build AS publish
ARG BUILD_CONFIGURATION=Release
RUN dotnet publish "./Logitar.Master.PortalSeeding.Worker.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false

FROM base AS final
WORKDIR /app
COPY --from=publish /app/publish .
ENTRYPOINT ["dotnet", "Logitar.Master.PortalSeeding.Worker.dll"]
Loading

0 comments on commit f1a7e28

Please sign in to comment.