13 Commits

Author SHA1 Message Date
agrynco 334bf2a567 Merge pull request 'refactor: replace local DAL abstractions with hrynco-ef packages' (#2) from use-hrynco-ef-packages into development 2026-05-05 20:40:07 +03:00
Anatolii Grynchuk ee4c988a0d refactor: replace local dal abstractions with hrynco-ef packages
Remove duplicate IEntity, Entity, ITransaction, IUnitOfWork, EfRepository,
EfUnitOfWork, EfTransactionAdapter — now consumed from HrynCo.DAL.Abstract
and HrynCo.DAL.EF packages (1.0.1).

Ref: IT-0
2026-05-05 20:39:06 +03:00
Anatolii Grynchuk 49982fc27f chore: add Application property to Serilog for log filtering
- Worker logs tagged as 'hrynco-notification-service-worker'
- Web logs tagged as 'hrynco-notification-service-web'

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-05-02 23:47:48 +03:00
Anatolii Grynchuk 6302a07178 feat: add test button to create channel form using ad-hoc smtp test endpoint
- Add TestSmtpCommand and TestSmtpHandler for ad-hoc smtp testing without saving
- Add POST /admin/channels/test-smtp endpoint accepting raw smtp settings
- Show Test button on both Create and Edit forms
- Test reads current form values so channel can be tested before saving
2026-05-02 19:53:20 +03:00
Anatolii Grynchuk 3e1cc696c1 fix: rename api service to web in all docker-compose files
- Aligns compose service name with the image name (hrynco.notification-service.web)
- Rename API_PORT env var to WEB_PORT for consistency

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-05-02 18:50:16 +03:00
Anatolii Grynchuk d71c3513a5 fix: add missing FK migration for EmailChannelUsage -> EmailChannel
- EF model had a pending HasOne/WithMany relationship not in migrations
- Adds FK_email_channel_usage_email_channels_provider_id with cascade delete

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-05-02 18:43:20 +03:00
Anatolii Grynchuk c5528b253d fix: add internal network to migrator, api, worker services
- migrator, api, worker were missing 'networks: - internal'
- db and rabbitmq are only on internal network, so services couldn't reach them
- also changed api depends_on db condition to service_healthy

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-05-02 18:31:34 +03:00
Anatolii Grynchuk 166b1a6103 fix: wait for postgres healthcheck before running migrator
- Add pg_isready healthcheck to db service (5s interval, 10 retries)
- Change migrator depends_on condition: service_started -> service_healthy
- Prevents migrator connection failure on fresh postgres startup

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-05-02 16:38:09 +03:00
Anatolii Grynchuk c88511ce3b chore: update package versions and formatting in Directory.Packages.props
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-05-02 15:40:12 +03:00
Anatolii Grynchuk ae119d1a3d feat: add production docker-compose with hrynco-services network
- Base compose: explicit internal network, named volumes with VOLUME_PREFIX
- docker-compose.prod.yml: production images, ports, restart policies, hrynco-services external network on rabbitmq
- docker-compose.Development.yml: cleaned up orphan volumes, named dev volumes

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-05-02 15:25:09 +03:00
Anatolii Grynchuk 74211f0a4a chore: add NuGet metadata to Contracts project
- Add PackageId, Authors, Description, PackageTags, RepositoryUrl
- Matches metadata pattern from HrynCo.Common and HrynCo.RabbitMq

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-05-02 14:40:01 +03:00
Anatolii Grynchuk 5003ab8764 fix: move ManagePackageVersionsCentrally to Directory.Packages.props
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-05-02 14:23:17 +03:00
Anatolii Grynchuk 4a431ec6c6 merge: IT-628 RabbitMQ worker, contracts, usage UI in channels screen 2026-05-02 14:01:04 +03:00
51 changed files with 629 additions and 400 deletions
+1 -5
View File
@@ -1,5 +1 @@
<Project> <Project />
<PropertyGroup>
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
</PropertyGroup>
</Project>
+7 -9
View File
@@ -1,16 +1,16 @@
<Project> <Project>
<PropertyGroup>
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
</PropertyGroup>
<ItemGroup> <ItemGroup>
<!-- Entity Framework Core --> <!-- Entity Framework Core -->
<PackageVersion Include="Microsoft.EntityFrameworkCore" Version="9.0.5" /> <PackageVersion Include="Microsoft.EntityFrameworkCore" Version="9.0.5" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.5" /> <PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.5" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Relational" Version="9.0.5" /> <PackageVersion Include="Microsoft.EntityFrameworkCore.Relational" Version="9.0.5" />
<PackageVersion Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="9.0.4" /> <PackageVersion Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="9.0.4" />
<!-- MediatR --> <!-- MediatR -->
<PackageVersion Include="MediatR" Version="12.4.1" /> <PackageVersion Include="MediatR" Version="12.4.1" />
<PackageVersion Include="MediatR.Extensions.Microsoft.DependencyInjection" Version="11.1.0" /> <PackageVersion Include="MediatR.Extensions.Microsoft.DependencyInjection" Version="11.1.0" />
<!-- Microsoft.Extensions --> <!-- Microsoft.Extensions -->
<PackageVersion Include="Microsoft.Extensions.Hosting" Version="10.0.6" /> <PackageVersion Include="Microsoft.Extensions.Hosting" Version="10.0.6" />
<PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="10.0.6" /> <PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="10.0.6" />
@@ -19,7 +19,6 @@
<PackageVersion Include="Scalar.AspNetCore" Version="2.14.9" /> <PackageVersion Include="Scalar.AspNetCore" Version="2.14.9" />
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation" Version="10.0.6" /> <PackageVersion Include="Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation" Version="10.0.6" />
<PackageVersion Include="Microsoft.AspNetCore.OpenApi" Version="10.0.6" /> <PackageVersion Include="Microsoft.AspNetCore.OpenApi" Version="10.0.6" />
<!-- Serilog --> <!-- Serilog -->
<PackageVersion Include="Serilog" Version="4.2.0" /> <PackageVersion Include="Serilog" Version="4.2.0" />
<PackageVersion Include="Serilog.AspNetCore" Version="9.0.0" /> <PackageVersion Include="Serilog.AspNetCore" Version="9.0.0" />
@@ -27,11 +26,11 @@
<PackageVersion Include="Serilog.Settings.Configuration" Version="9.0.0" /> <PackageVersion Include="Serilog.Settings.Configuration" Version="9.0.0" />
<PackageVersion Include="Serilog.Sinks.Console" Version="6.0.0" /> <PackageVersion Include="Serilog.Sinks.Console" Version="6.0.0" />
<PackageVersion Include="Serilog.Sinks.Seq" Version="9.0.0" /> <PackageVersion Include="Serilog.Sinks.Seq" Version="9.0.0" />
<!-- HrynCo shared packages --> <!-- HrynCo shared packages -->
<PackageVersion Include="HrynCo.Common" Version="1.0.0" /> <PackageVersion Include="HrynCo.Common" Version="1.0.11" />
<PackageVersion Include="HrynCo.RabbitMq" Version="1.0.14" /> <PackageVersion Include="HrynCo.RabbitMq" Version="1.0.15" />
<PackageVersion Include="HrynCo.DAL.Abstract" Version="1.0.1" />
<PackageVersion Include="HrynCo.DAL.EF" Version="1.0.1" />
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.14.1" /> <PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
<PackageVersion Include="coverlet.collector" Version="6.0.4" /> <PackageVersion Include="coverlet.collector" Version="6.0.4" />
<PackageVersion Include="xunit" Version="2.9.3" /> <PackageVersion Include="xunit" Version="2.9.3" />
@@ -40,5 +39,4 @@
<PackageVersion Include="Testcontainers.PostgreSql" Version="4.6.0" /> <PackageVersion Include="Testcontainers.PostgreSql" Version="4.6.0" />
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="9.0.5" /> <PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="9.0.5" />
</ItemGroup> </ItemGroup>
</Project> </Project>
@@ -4,6 +4,12 @@
<TargetFramework>net10.0</TargetFramework> <TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<PackageId>HrynCo.NotificationService.Contracts</PackageId>
<Authors>HrynCo</Authors>
<Description>RabbitMQ message contracts for HrynCo.NotificationService.</Description>
<PackageTags>hrynco notification email rabbitmq contracts</PackageTags>
<RepositoryType>git</RepositoryType>
<RepositoryUrl>https://gitea.grynco.com.ua/hrynco/hrynco-notification-service.git</RepositoryUrl>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
@@ -1,17 +0,0 @@
namespace HrynCo.NotificationService.DAL.Abstract.Entities;
public abstract class Entity<TId> : IEntity<TId> where TId : struct
{
public TId Id { get; set; }
public DateTimeOffset Created { get; set; }
public DateTimeOffset? Updated { get; set; }
}
public abstract class Entity : Entity<Guid>
{
protected Entity()
{
Id = Guid.NewGuid();
Created = DateTimeOffset.UtcNow;
}
}
@@ -1,8 +0,0 @@
namespace HrynCo.NotificationService.DAL.Abstract.Entities;
public interface IEntity<TId> where TId : struct
{
TId Id { get; set; }
DateTimeOffset Created { get; set; }
DateTimeOffset? Updated { get; set; }
}
@@ -1,5 +1,9 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<ItemGroup>
<PackageReference Include="HrynCo.DAL.Abstract" />
</ItemGroup>
<PropertyGroup> <PropertyGroup>
<TargetFramework>net10.0</TargetFramework> <TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
@@ -1,7 +0,0 @@
namespace HrynCo.NotificationService.DAL.Abstract;
public interface ITransaction : IAsyncDisposable
{
Task CommitAsync(CancellationToken cancellationToken = default);
Task RollbackAsync(CancellationToken cancellationToken = default);
}
@@ -1,11 +0,0 @@
namespace HrynCo.NotificationService.DAL.Abstract;
public interface IUnitOfWork
{
Task SaveChangesAsync(CancellationToken cancellationToken = default);
Task<ITransaction> BeginTransactionAsync(CancellationToken cancellationToken = default);
ITransaction? GetCurrentTransaction();
Task ExecuteInTransactionAsync(Func<Task> action);
Task<TResult> ExecuteInTransactionAsync<TResult>(Func<Task<TResult>> action);
}
@@ -1,4 +1,4 @@
using HrynCo.NotificationService.DAL.Abstract.Entities; using HrynCo.DAL.Abstract.Entities;
namespace HrynCo.NotificationService.DAL.Abstract.Providers; namespace HrynCo.NotificationService.DAL.Abstract.Providers;
@@ -1,4 +1,4 @@
using HrynCo.NotificationService.DAL.Abstract.Entities; using HrynCo.DAL.Abstract.Entities;
namespace HrynCo.NotificationService.DAL.Abstract.Providers; namespace HrynCo.NotificationService.DAL.Abstract.Providers;
@@ -1,4 +1,4 @@
using HrynCo.NotificationService.DAL.Abstract.Entities; using HrynCo.DAL.Abstract.Entities;
namespace HrynCo.NotificationService.DAL.Abstract.Templates; namespace HrynCo.NotificationService.DAL.Abstract.Templates;
@@ -1,45 +0,0 @@
using System.Linq.Expressions;
using Microsoft.EntityFrameworkCore;
namespace HrynCo.NotificationService.DAL.EF.Core;
internal abstract class EfRepository<TEntity>
where TEntity : class
{
protected NotificationDbContext DbContext { get; }
protected DbSet<TEntity> DbSet { get; }
protected EfRepository(NotificationDbContext dbContext)
{
DbContext = dbContext;
DbSet = dbContext.Set<TEntity>();
}
protected async Task AddAsync(TEntity entity, CancellationToken ct = default)
{
await DbSet.AddAsync(entity, ct);
}
protected async Task AddRangeAsync(IEnumerable<TEntity> entities, CancellationToken ct = default)
{
await DbSet.AddRangeAsync(entities, ct);
}
protected void Update(TEntity entity)
{
DbSet.Update(entity);
}
protected void Delete(TEntity entity)
{
DbSet.Remove(entity);
}
protected void DeleteRange(IEnumerable<TEntity> entities)
{
DbSet.RemoveRange(entities);
}
protected Task<bool> ExistsAsync(Expression<Func<TEntity, bool>> predicate, CancellationToken ct = default) =>
DbSet.AnyAsync(predicate, ct);
}
@@ -1,29 +0,0 @@
using HrynCo.NotificationService.DAL.Abstract;
using Microsoft.EntityFrameworkCore.Storage;
namespace HrynCo.NotificationService.DAL.EF.Core;
internal sealed class EfTransactionAdapter : ITransaction
{
private readonly IDbContextTransaction _transaction;
internal EfTransactionAdapter(IDbContextTransaction transaction)
{
_transaction = transaction;
}
public Task CommitAsync(CancellationToken cancellationToken = default)
{
return _transaction.CommitAsync(cancellationToken);
}
public Task RollbackAsync(CancellationToken cancellationToken = default)
{
return _transaction.RollbackAsync(cancellationToken);
}
public ValueTask DisposeAsync()
{
return _transaction.DisposeAsync();
}
}
@@ -1,105 +0,0 @@
using HrynCo.NotificationService.DAL.Abstract;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Storage;
namespace HrynCo.NotificationService.DAL.EF.Core;
internal abstract class EfUnitOfWork<TDbContext> : IUnitOfWork
where TDbContext : DbContext
{
private readonly TDbContext _context;
private EfTransactionAdapter? _currentTransaction;
protected EfUnitOfWork(TDbContext context)
{
_context = context;
}
public Task SaveChangesAsync(CancellationToken cancellationToken = default)
{
return _context.SaveChangesAsync(cancellationToken);
}
public async Task<ITransaction> BeginTransactionAsync(CancellationToken cancellationToken = default)
{
if (_currentTransaction != null)
{
return _currentTransaction;
}
IDbContextTransaction tx = await _context.Database.BeginTransactionAsync(cancellationToken);
_currentTransaction = new EfTransactionAdapter(tx);
return _currentTransaction;
}
public ITransaction? GetCurrentTransaction()
{
return _currentTransaction;
}
public async Task ExecuteInTransactionAsync(Func<Task> action)
{
ITransaction? existing = GetCurrentTransaction();
bool ownsTransaction = existing is null;
ITransaction tx = existing ?? await BeginTransactionAsync();
try
{
await action();
if (ownsTransaction)
{
await tx.CommitAsync();
}
}
catch
{
if (ownsTransaction)
{
await tx.RollbackAsync();
}
throw;
}
finally
{
if (ownsTransaction)
{
await tx.DisposeAsync();
}
}
}
public async Task<TResult> ExecuteInTransactionAsync<TResult>(Func<Task<TResult>> action)
{
ITransaction? existing = GetCurrentTransaction();
bool ownsTransaction = existing is null;
ITransaction tx = existing ?? await BeginTransactionAsync();
try
{
TResult result = await action();
if (ownsTransaction)
{
await tx.CommitAsync();
}
return result;
}
catch
{
if (ownsTransaction)
{
await tx.RollbackAsync();
}
throw;
}
finally
{
if (ownsTransaction)
{
await tx.DisposeAsync();
}
}
}
}
@@ -1,3 +1,5 @@
using HrynCo.DAL.EF.Core;
namespace HrynCo.NotificationService.DAL.EF.Core; namespace HrynCo.NotificationService.DAL.EF.Core;
internal sealed class UnitOfWork : EfUnitOfWork<NotificationDbContext> internal sealed class UnitOfWork : EfUnitOfWork<NotificationDbContext>
@@ -1,4 +1,4 @@
using HrynCo.NotificationService.DAL.Abstract.Entities; using HrynCo.DAL.Abstract.Entities;
using HrynCo.NotificationService.DAL.Abstract.Providers; using HrynCo.NotificationService.DAL.Abstract.Providers;
namespace HrynCo.NotificationService.DAL.EF.Entities; namespace HrynCo.NotificationService.DAL.EF.Entities;
@@ -1,4 +1,4 @@
using HrynCo.NotificationService.DAL.Abstract.Entities; using HrynCo.DAL.Abstract.Entities;
namespace HrynCo.NotificationService.DAL.EF.Entities; namespace HrynCo.NotificationService.DAL.EF.Entities;
@@ -1,4 +1,4 @@
using HrynCo.NotificationService.DAL.Abstract.Entities; using HrynCo.DAL.Abstract.Entities;
namespace HrynCo.NotificationService.DAL.EF.Entities; namespace HrynCo.NotificationService.DAL.EF.Entities;
@@ -5,6 +5,7 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="HrynCo.DAL.EF" />
<PackageReference Include="Microsoft.EntityFrameworkCore" /> <PackageReference Include="Microsoft.EntityFrameworkCore" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design"> <PackageReference Include="Microsoft.EntityFrameworkCore.Design">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
@@ -0,0 +1,225 @@
// <auto-generated />
using System;
using HrynCo.NotificationService.DAL.EF;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace HrynCo.NotificationService.DAL.EF.Migrations
{
[DbContext(typeof(NotificationDbContext))]
[Migration("20260502154249_PendingChanges")]
partial class PendingChanges
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "9.0.5")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("HrynCo.NotificationService.DAL.EF.Entities.EmailChannelEntity", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid")
.HasColumnName("id");
b.Property<DateTimeOffset>("Created")
.HasColumnType("timestamp with time zone")
.HasColumnName("created");
b.Property<int?>("DailyLimit")
.HasColumnType("integer")
.HasColumnName("daily_limit");
b.Property<int>("EmailChannelType")
.HasColumnType("integer")
.HasColumnName("provider_type");
b.Property<bool>("IsActive")
.HasColumnType("boolean")
.HasColumnName("is_active");
b.Property<int?>("MonthlyLimit")
.HasColumnType("integer")
.HasColumnName("monthly_limit");
b.Property<int>("Priority")
.HasColumnType("integer")
.HasColumnName("priority");
b.Property<string>("ServiceName")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("character varying(100)")
.HasColumnName("service_name");
b.Property<string>("SettingsJson")
.IsRequired()
.HasColumnType("jsonb")
.HasColumnName("settings");
b.Property<DateTimeOffset?>("Updated")
.HasColumnType("timestamp with time zone")
.HasColumnName("updated");
b.Property<int>("WarnThresholdPercent")
.HasColumnType("integer")
.HasColumnName("warn_threshold_percent");
b.HasKey("Id");
b.HasIndex("ServiceName", "Priority");
b.ToTable("email_channels", (string)null);
});
modelBuilder.Entity("HrynCo.NotificationService.DAL.EF.Entities.EmailChannelUsageEntity", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid")
.HasColumnName("id");
b.Property<DateTimeOffset>("Created")
.HasColumnType("timestamp with time zone")
.HasColumnName("created");
b.Property<DateOnly>("Date")
.HasColumnType("date")
.HasColumnName("date");
b.Property<Guid>("ProviderId")
.HasColumnType("uuid")
.HasColumnName("provider_id");
b.Property<int>("SentCount")
.HasColumnType("integer")
.HasColumnName("sent_count");
b.Property<DateTimeOffset?>("Updated")
.HasColumnType("timestamp with time zone")
.HasColumnName("updated");
b.HasKey("Id");
b.HasIndex("ProviderId", "Date")
.IsUnique();
b.ToTable("email_channel_usage", (string)null);
});
modelBuilder.Entity("HrynCo.NotificationService.DAL.EF.Entities.EmailTemplateEntity", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid")
.HasColumnName("id");
b.Property<DateTimeOffset>("Created")
.HasColumnType("timestamp with time zone")
.HasColumnName("created");
b.Property<string>("HtmlBody")
.IsRequired()
.HasColumnType("text")
.HasColumnName("html_body");
b.Property<string>("Key")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("character varying(100)")
.HasColumnName("key");
b.Property<string>("LanguageCode")
.IsRequired()
.HasMaxLength(10)
.HasColumnType("character varying(10)")
.HasColumnName("language_code");
b.Property<string>("ServiceName")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("character varying(100)")
.HasColumnName("service_name");
b.Property<string>("Subject")
.IsRequired()
.HasColumnType("text")
.HasColumnName("subject");
b.Property<string>("TextBody")
.IsRequired()
.HasColumnType("text")
.HasColumnName("text_body");
b.Property<DateTimeOffset?>("Updated")
.HasColumnType("timestamp with time zone")
.HasColumnName("updated");
b.HasKey("Id");
b.HasIndex("ServiceName", "Key", "LanguageCode")
.IsUnique();
b.ToTable("email_templates", (string)null);
});
modelBuilder.Entity("HrynCo.NotificationService.DAL.EF.Entities.EmailChannelUsageEntity", b =>
{
b.HasOne("HrynCo.NotificationService.DAL.EF.Entities.EmailChannelEntity", null)
.WithMany("UsageRecords")
.HasForeignKey("ProviderId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("HrynCo.NotificationService.DAL.EF.Entities.EmailTemplateEntity", b =>
{
b.OwnsMany("HrynCo.NotificationService.DAL.EF.Entities.EmailTemplateVariableData", "Variables", b1 =>
{
b1.Property<Guid>("EmailTemplateEntityId")
.HasColumnType("uuid");
b1.Property<int>("__synthesizedOrdinal")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
b1.Property<string>("Name")
.IsRequired()
.HasColumnType("text")
.HasAnnotation("Relational:JsonPropertyName", "name");
b1.Property<bool>("Required")
.HasColumnType("boolean")
.HasAnnotation("Relational:JsonPropertyName", "required");
b1.HasKey("EmailTemplateEntityId", "__synthesizedOrdinal");
b1.ToTable("email_templates");
b1.ToJson("variables");
b1.WithOwner()
.HasForeignKey("EmailTemplateEntityId");
});
b.Navigation("Variables");
});
modelBuilder.Entity("HrynCo.NotificationService.DAL.EF.Entities.EmailChannelEntity", b =>
{
b.Navigation("UsageRecords");
});
#pragma warning restore 612, 618
}
}
}
@@ -0,0 +1,30 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace HrynCo.NotificationService.DAL.EF.Migrations
{
/// <inheritdoc />
public partial class PendingChanges : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddForeignKey(
name: "FK_email_channel_usage_email_channels_provider_id",
table: "email_channel_usage",
column: "provider_id",
principalTable: "email_channels",
principalColumn: "id",
onDelete: ReferentialAction.Cascade);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropForeignKey(
name: "FK_email_channel_usage_email_channels_provider_id",
table: "email_channel_usage");
}
}
}
@@ -170,6 +170,15 @@ namespace HrynCo.NotificationService.DAL.EF.Migrations
b.ToTable("email_templates", (string)null); b.ToTable("email_templates", (string)null);
}); });
modelBuilder.Entity("HrynCo.NotificationService.DAL.EF.Entities.EmailChannelUsageEntity", b =>
{
b.HasOne("HrynCo.NotificationService.DAL.EF.Entities.EmailChannelEntity", null)
.WithMany("UsageRecords")
.HasForeignKey("ProviderId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("HrynCo.NotificationService.DAL.EF.Entities.EmailTemplateEntity", b => modelBuilder.Entity("HrynCo.NotificationService.DAL.EF.Entities.EmailTemplateEntity", b =>
{ {
b.OwnsMany("HrynCo.NotificationService.DAL.EF.Entities.EmailTemplateVariableData", "Variables", b1 => b.OwnsMany("HrynCo.NotificationService.DAL.EF.Entities.EmailTemplateVariableData", "Variables", b1 =>
@@ -202,6 +211,11 @@ namespace HrynCo.NotificationService.DAL.EF.Migrations
b.Navigation("Variables"); b.Navigation("Variables");
}); });
modelBuilder.Entity("HrynCo.NotificationService.DAL.EF.Entities.EmailChannelEntity", b =>
{
b.Navigation("UsageRecords");
});
#pragma warning restore 612, 618 #pragma warning restore 612, 618
} }
} }
@@ -1,13 +1,13 @@
using System.Text.Json; using System.Text.Json;
using HrynCo.NotificationService.DAL.Abstract.Providers; using HrynCo.NotificationService.DAL.Abstract.Providers;
using HrynCo.NotificationService.DAL.Abstract.Repositories; using HrynCo.NotificationService.DAL.Abstract.Repositories;
using HrynCo.NotificationService.DAL.EF.Core; using HrynCo.DAL.EF.Core;
using HrynCo.NotificationService.DAL.EF.Entities; using HrynCo.NotificationService.DAL.EF.Entities;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
namespace HrynCo.NotificationService.DAL.EF.Repositories; namespace HrynCo.NotificationService.DAL.EF.Repositories;
internal sealed class EmailChannelRepository : EfRepository<EmailChannelEntity>, IEmailChannelRepository internal sealed class EmailChannelRepository : EfRepository<NotificationDbContext, EmailChannelEntity>, IEmailChannelRepository
{ {
public EmailChannelRepository(NotificationDbContext dbContext) : base(dbContext) public EmailChannelRepository(NotificationDbContext dbContext) : base(dbContext)
{ {
@@ -1,11 +1,11 @@
using HrynCo.NotificationService.DAL.Abstract.Repositories; using HrynCo.NotificationService.DAL.Abstract.Repositories;
using HrynCo.NotificationService.DAL.EF.Core; using HrynCo.DAL.EF.Core;
using HrynCo.NotificationService.DAL.EF.Entities; using HrynCo.NotificationService.DAL.EF.Entities;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
namespace HrynCo.NotificationService.DAL.EF.Repositories; namespace HrynCo.NotificationService.DAL.EF.Repositories;
internal sealed class EmailChannelUsageRepository : EfRepository<EmailChannelUsageEntity>, IEmailChannelUsageRepository internal sealed class EmailChannelUsageRepository : EfRepository<NotificationDbContext, EmailChannelUsageEntity>, IEmailChannelUsageRepository
{ {
public EmailChannelUsageRepository(NotificationDbContext dbContext) : base(dbContext) public EmailChannelUsageRepository(NotificationDbContext dbContext) : base(dbContext)
{ {
@@ -1,12 +1,12 @@
using HrynCo.NotificationService.DAL.Abstract.Repositories; using HrynCo.NotificationService.DAL.Abstract.Repositories;
using HrynCo.NotificationService.DAL.Abstract.Templates; using HrynCo.NotificationService.DAL.Abstract.Templates;
using HrynCo.NotificationService.DAL.EF.Core; using HrynCo.DAL.EF.Core;
using HrynCo.NotificationService.DAL.EF.Entities; using HrynCo.NotificationService.DAL.EF.Entities;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
namespace HrynCo.NotificationService.DAL.EF.Repositories; namespace HrynCo.NotificationService.DAL.EF.Repositories;
internal sealed class EmailTemplateRepository : EfRepository<EmailTemplateEntity>, IEmailTemplateRepository internal sealed class EmailTemplateRepository : EfRepository<NotificationDbContext, EmailTemplateEntity>, IEmailTemplateRepository
{ {
public EmailTemplateRepository(NotificationDbContext dbContext) : base(dbContext) public EmailTemplateRepository(NotificationDbContext dbContext) : base(dbContext)
{ {
@@ -1,4 +1,4 @@
using HrynCo.NotificationService.DAL.Abstract; using HrynCo.DAL.Abstract;
using HrynCo.NotificationService.DAL.Abstract.Repositories; using HrynCo.NotificationService.DAL.Abstract.Repositories;
using HrynCo.NotificationService.DAL.EF.Core; using HrynCo.NotificationService.DAL.EF.Core;
using HrynCo.NotificationService.DAL.EF.Repositories; using HrynCo.NotificationService.DAL.EF.Repositories;
@@ -1,5 +1,5 @@
using HrynCo.Common; using HrynCo.Common;
using HrynCo.NotificationService.DAL.Abstract; using HrynCo.DAL.Abstract;
using MediatR; using MediatR;
namespace HrynCo.NotificationService.Services.Behaviors; namespace HrynCo.NotificationService.Services.Behaviors;
@@ -1,4 +1,4 @@
using HrynCo.NotificationService.DAL.Abstract; using HrynCo.DAL.Abstract;
using HrynCo.NotificationService.Services.Logging; using HrynCo.NotificationService.Services.Logging;
using MediatR; using MediatR;
using Serilog; using Serilog;
@@ -1,4 +1,4 @@
using HrynCo.NotificationService.DAL.Abstract; using HrynCo.DAL.Abstract;
using HrynCo.NotificationService.DAL.Abstract.Providers; using HrynCo.NotificationService.DAL.Abstract.Providers;
using HrynCo.NotificationService.DAL.Abstract.Repositories; using HrynCo.NotificationService.DAL.Abstract.Repositories;
using HrynCo.NotificationService.Services.Core; using HrynCo.NotificationService.Services.Core;
@@ -1,4 +1,4 @@
using HrynCo.NotificationService.DAL.Abstract; using HrynCo.DAL.Abstract;
using HrynCo.NotificationService.DAL.Abstract.Repositories; using HrynCo.NotificationService.DAL.Abstract.Repositories;
using HrynCo.NotificationService.Services.Core; using HrynCo.NotificationService.Services.Core;
using HrynCo.NotificationService.Services.Logging; using HrynCo.NotificationService.Services.Logging;
@@ -1,4 +1,4 @@
using HrynCo.NotificationService.DAL.Abstract; using HrynCo.DAL.Abstract;
using HrynCo.NotificationService.DAL.Abstract.Providers; using HrynCo.NotificationService.DAL.Abstract.Providers;
using HrynCo.NotificationService.DAL.Abstract.Repositories; using HrynCo.NotificationService.DAL.Abstract.Repositories;
using HrynCo.NotificationService.Services.Core; using HrynCo.NotificationService.Services.Core;
@@ -1,4 +1,4 @@
using HrynCo.NotificationService.DAL.Abstract; using HrynCo.DAL.Abstract;
using HrynCo.NotificationService.DAL.Abstract.Providers; using HrynCo.NotificationService.DAL.Abstract.Providers;
using HrynCo.NotificationService.DAL.Abstract.Repositories; using HrynCo.NotificationService.DAL.Abstract.Repositories;
using HrynCo.NotificationService.Services.Core; using HrynCo.NotificationService.Services.Core;
@@ -1,4 +1,4 @@
using HrynCo.NotificationService.DAL.Abstract; using HrynCo.DAL.Abstract;
using HrynCo.NotificationService.DAL.Abstract.Providers; using HrynCo.NotificationService.DAL.Abstract.Providers;
using HrynCo.NotificationService.DAL.Abstract.Repositories; using HrynCo.NotificationService.DAL.Abstract.Repositories;
using HrynCo.NotificationService.Services.Core; using HrynCo.NotificationService.Services.Core;
@@ -1,4 +1,4 @@
using HrynCo.NotificationService.DAL.Abstract; using HrynCo.DAL.Abstract;
using HrynCo.NotificationService.DAL.Abstract.Repositories; using HrynCo.NotificationService.DAL.Abstract.Repositories;
using HrynCo.NotificationService.Services.Core; using HrynCo.NotificationService.Services.Core;
using HrynCo.NotificationService.Services.Logging; using HrynCo.NotificationService.Services.Logging;
@@ -1,6 +1,6 @@
using System.Net; using System.Net;
using System.Net.Mail; using System.Net.Mail;
using HrynCo.NotificationService.DAL.Abstract; using HrynCo.DAL.Abstract;
using HrynCo.NotificationService.DAL.Abstract.Providers; using HrynCo.NotificationService.DAL.Abstract.Providers;
using HrynCo.NotificationService.DAL.Abstract.Repositories; using HrynCo.NotificationService.DAL.Abstract.Repositories;
using HrynCo.NotificationService.Services.Core; using HrynCo.NotificationService.Services.Core;
@@ -0,0 +1,18 @@
using HrynCo.NotificationService.Services.Core;
using MediatR;
namespace HrynCo.NotificationService.Services.EmailChannels.TestSmtp;
/// <summary>
/// Sends a test email using the provided SMTP settings without persisting anything.
/// </summary>
public sealed record TestSmtpCommand(
string Host,
int Port,
string Username,
string Password,
bool UseSsl,
string FromEmail,
string FromName,
string ToEmail
) : IRequest<ServiceResult<Core.Unit>>;
@@ -0,0 +1,52 @@
using System.Net;
using System.Net.Mail;
using HrynCo.DAL.Abstract;
using HrynCo.NotificationService.Services.Core;
using HrynCo.NotificationService.Services.Logging;
using static HrynCo.NotificationService.Services.Core.ServiceResultHelper;
namespace HrynCo.NotificationService.Services.EmailChannels.TestSmtp;
internal sealed class TestSmtpHandler
: RequestHandler<TestSmtpCommand, ServiceResult<Unit>>
{
public TestSmtpHandler(
IContextualSerilogLogger<TestSmtpCommand> logger,
IUnitOfWork unitOfWork)
: base(logger, unitOfWork)
{
}
protected override async Task<ServiceResult<Unit>> DoOnHandle(
TestSmtpCommand request, CancellationToken cancellationToken)
{
try
{
using var client = new SmtpClient(request.Host, request.Port)
{
EnableSsl = request.UseSsl,
Credentials = string.IsNullOrWhiteSpace(request.Username)
? null
: new NetworkCredential(request.Username, request.Password)
};
using var mail = new MailMessage
{
From = new MailAddress(request.FromEmail, request.FromName),
Subject = "✅ Test email from Notification Service",
Body = "<p>This is a test email sent from the <b>Notification Service</b> admin panel to verify the channel settings.</p>",
IsBodyHtml = true
};
mail.To.Add(new MailAddress(request.ToEmail));
await client.SendMailAsync(mail, cancellationToken);
}
catch (Exception ex)
{
Logger.Error(ex, "Ad-hoc SMTP test failed for host {Host}", request.Host);
return Failure<Unit>(ex.Message);
}
return Success(Unit.Value);
}
}
@@ -1,4 +1,4 @@
using HrynCo.NotificationService.DAL.Abstract; using HrynCo.DAL.Abstract;
using HrynCo.NotificationService.DAL.Abstract.Repositories; using HrynCo.NotificationService.DAL.Abstract.Repositories;
using HrynCo.NotificationService.Services.Core; using HrynCo.NotificationService.Services.Core;
using HrynCo.NotificationService.Services.Logging; using HrynCo.NotificationService.Services.Logging;
@@ -1,4 +1,4 @@
using HrynCo.NotificationService.DAL.Abstract; using HrynCo.DAL.Abstract;
using HrynCo.NotificationService.DAL.Abstract.Repositories; using HrynCo.NotificationService.DAL.Abstract.Repositories;
using HrynCo.NotificationService.DAL.Abstract.Templates; using HrynCo.NotificationService.DAL.Abstract.Templates;
using HrynCo.NotificationService.Services.Core; using HrynCo.NotificationService.Services.Core;
@@ -1,4 +1,4 @@
using HrynCo.NotificationService.DAL.Abstract; using HrynCo.DAL.Abstract;
using HrynCo.NotificationService.DAL.Abstract.Repositories; using HrynCo.NotificationService.DAL.Abstract.Repositories;
using HrynCo.NotificationService.Services.Core; using HrynCo.NotificationService.Services.Core;
using HrynCo.NotificationService.Services.Logging; using HrynCo.NotificationService.Services.Logging;
@@ -1,4 +1,4 @@
using HrynCo.NotificationService.DAL.Abstract; using HrynCo.DAL.Abstract;
using HrynCo.NotificationService.DAL.Abstract.Repositories; using HrynCo.NotificationService.DAL.Abstract.Repositories;
using HrynCo.NotificationService.DAL.Abstract.Templates; using HrynCo.NotificationService.DAL.Abstract.Templates;
using HrynCo.NotificationService.Services.Core; using HrynCo.NotificationService.Services.Core;
@@ -1,4 +1,4 @@
using HrynCo.NotificationService.DAL.Abstract; using HrynCo.DAL.Abstract;
using HrynCo.NotificationService.DAL.Abstract.Repositories; using HrynCo.NotificationService.DAL.Abstract.Repositories;
using HrynCo.NotificationService.DAL.Abstract.Templates; using HrynCo.NotificationService.DAL.Abstract.Templates;
using HrynCo.NotificationService.Services.Core; using HrynCo.NotificationService.Services.Core;
@@ -1,4 +1,4 @@
using HrynCo.NotificationService.DAL.Abstract; using HrynCo.DAL.Abstract;
using HrynCo.NotificationService.DAL.Abstract.Repositories; using HrynCo.NotificationService.DAL.Abstract.Repositories;
using HrynCo.NotificationService.DAL.Abstract.Templates; using HrynCo.NotificationService.DAL.Abstract.Templates;
using HrynCo.NotificationService.Services.Core; using HrynCo.NotificationService.Services.Core;
@@ -1,4 +1,4 @@
using HrynCo.NotificationService.DAL.Abstract; using HrynCo.DAL.Abstract;
using HrynCo.NotificationService.DAL.Abstract.Repositories; using HrynCo.NotificationService.DAL.Abstract.Repositories;
using HrynCo.NotificationService.Services.Core; using HrynCo.NotificationService.Services.Core;
using HrynCo.NotificationService.Services.Logging; using HrynCo.NotificationService.Services.Logging;
@@ -5,6 +5,7 @@ using HrynCo.NotificationService.Services.EmailChannels.Delete;
using HrynCo.NotificationService.Services.EmailChannels.Get; using HrynCo.NotificationService.Services.EmailChannels.Get;
using HrynCo.NotificationService.Services.EmailChannels.GetUsageSummary; using HrynCo.NotificationService.Services.EmailChannels.GetUsageSummary;
using HrynCo.NotificationService.Services.EmailChannels.Send; using HrynCo.NotificationService.Services.EmailChannels.Send;
using HrynCo.NotificationService.Services.EmailChannels.TestSmtp;
using HrynCo.NotificationService.Services.EmailChannels.Update; using HrynCo.NotificationService.Services.EmailChannels.Update;
using HrynCo.NotificationService.Web.Controllers.Admin.ViewModels; using HrynCo.NotificationService.Web.Controllers.Admin.ViewModels;
using MediatR; using MediatR;
@@ -133,6 +134,20 @@ public class AdminChannelsController(IMediator mediator) : Controller
return RedirectToAction(nameof(Index)); return RedirectToAction(nameof(Index));
} }
// POST /admin/channels/test-smtp
[HttpPost("test-smtp")]
public async Task<IActionResult> TestSmtp([FromBody] TestSmtpRequest request, CancellationToken ct)
{
var result = await mediator.Send(new TestSmtpCommand(
request.Host, request.Port, request.Username, request.Password,
request.UseSsl, request.FromEmail, request.FromName, request.ToEmail), ct);
if (!result.IsSuccess)
return Ok(new { success = false, message = result.Error?.Message });
return Ok(new { success = true, message = $"Test email sent to {request.ToEmail}." });
}
// POST /admin/channels/{id}/test // POST /admin/channels/{id}/test
[HttpPost("{id:guid}/test")] [HttpPost("{id:guid}/test")]
public async Task<IActionResult> Test(Guid id, [FromBody] TestChannelRequest request, CancellationToken ct) public async Task<IActionResult> Test(Guid id, [FromBody] TestChannelRequest request, CancellationToken ct)
@@ -165,3 +180,6 @@ public class AdminChannelsController(IMediator mediator) : Controller
} }
public record TestChannelRequest(string ToEmail); public record TestChannelRequest(string ToEmail);
public record TestSmtpRequest(
string Host, int Port, string Username, string Password,
bool UseSsl, string FromEmail, string FromName, string ToEmail);
@@ -121,21 +121,16 @@
<button type="submit" form="channelForm" class="btn btn-primary"> <button type="submit" form="channelForm" class="btn btn-primary">
<i class="bi bi-floppy me-1"></i> Save <i class="bi bi-floppy me-1"></i> Save
</button> </button>
@if (!Model.IsNew)
{
<button type="button" class="btn btn-success" id="testModalBtn"> <button type="button" class="btn btn-success" id="testModalBtn">
<i class="bi bi-send me-1"></i> Test <i class="bi bi-send me-1"></i> Test
</button> </button>
}
<a href="/admin/channels" class="btn btn-secondary"> <a href="/admin/channels" class="btn btn-secondary">
<i class="bi bi-x-lg me-1"></i> Cancel <i class="bi bi-x-lg me-1"></i> Cancel
</a> </a>
} }
</form> </form>
@if (!Model.IsNew) <div class="modal fade" id="testModal" tabindex="-1">
{
<div class="modal fade" id="testModal" tabindex="-1">
<div class="modal-dialog modal-dialog-centered"> <div class="modal-dialog modal-dialog-centered">
<div class="modal-content"> <div class="modal-content">
<div class="modal-header"> <div class="modal-header">
@@ -143,28 +138,30 @@
<button type="button" class="btn-close" data-bs-dismiss="modal"></button> <button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div> </div>
<div class="modal-body"> <div class="modal-body">
<p class="text-muted small">Tests the current form settings — no need to save first.</p>
<label for="testToEmail" class="form-label fw-semibold">Recipient Email</label> <label for="testToEmail" class="form-label fw-semibold">Recipient Email</label>
<input type="email" id="testToEmail" class="form-control" placeholder="you@example.com" /> <input type="email" id="testToEmail" class="form-control" placeholder="you@example.com" />
<div id="testResult" class="mt-3" style="display:none"></div> <div id="testResult" class="mt-3" style="display:none"></div>
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button> <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
<button type="button" class="btn btn-success" id="testSendBtn" onclick="sendTestEmail('@Model.Id')"> <button type="button" class="btn btn-success" id="testSendBtn" onclick="sendTestEmail()">
<i class="bi bi-send me-1"></i> Send <i class="bi bi-send me-1"></i> Send
</button> </button>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<script> <script>
document.addEventListener('click', function (e) { document.addEventListener('click', function (e) {
if (e.target.closest('#testModalBtn')) { if (e.target.closest('#testModalBtn')) {
document.getElementById('testResult').style.display = 'none';
bootstrap.Modal.getOrCreateInstance(document.getElementById('testModal')).show(); bootstrap.Modal.getOrCreateInstance(document.getElementById('testModal')).show();
} }
}); });
async function sendTestEmail(channelId) { async function sendTestEmail() {
const toEmail = document.getElementById('testToEmail').value.trim(); const toEmail = document.getElementById('testToEmail').value.trim();
const resultDiv = document.getElementById('testResult'); const resultDiv = document.getElementById('testResult');
const sendBtn = document.getElementById('testSendBtn'); const sendBtn = document.getElementById('testSendBtn');
@@ -175,15 +172,27 @@
return; return;
} }
const form = document.getElementById('channelForm');
const payload = {
host: form.querySelector('[name="Host"]').value,
port: parseInt(form.querySelector('[name="Port"]').value, 10),
username: form.querySelector('[name="Username"]').value,
password: form.querySelector('[name="Password"]').value,
useSsl: form.querySelector('[name="UseSsl"]').checked,
fromEmail: form.querySelector('[name="FromEmail"]').value,
fromName: form.querySelector('[name="FromName"]').value,
toEmail
};
sendBtn.disabled = true; sendBtn.disabled = true;
sendBtn.innerHTML = '<span class="spinner-border spinner-border-sm me-1"></span> Sending…'; sendBtn.innerHTML = '<span class="spinner-border spinner-border-sm me-1"></span> Sending…';
resultDiv.style.display = 'none'; resultDiv.style.display = 'none';
try { try {
const resp = await fetch(`/admin/channels/${channelId}/test`, { const resp = await fetch('/admin/channels/test-smtp', {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ toEmail }) body: JSON.stringify(payload)
}); });
const data = await resp.json(); const data = await resp.json();
resultDiv.style.display = 'block'; resultDiv.style.display = 'block';
@@ -198,5 +207,4 @@
sendBtn.innerHTML = '<i class="bi bi-send me-1"></i> Send'; sendBtn.innerHTML = '<i class="bi bi-send me-1"></i> Send';
} }
} }
</script> </script>
}
@@ -20,7 +20,10 @@
} }
} }
], ],
"Enrich": [ "FromLogContext" ] "Enrich": [ "FromLogContext" ],
"Properties": {
"Application": "hrynco-notification-service-web"
}
}, },
"AllowedHosts": "*" "AllowedHosts": "*"
} }
@@ -27,6 +27,9 @@
} }
} }
], ],
"Enrich": [ "FromLogContext" ] "Enrich": [ "FromLogContext" ],
"Properties": {
"Application": "hrynco-notification-service-worker"
}
} }
} }
@@ -5,7 +5,7 @@ services:
environment: environment:
- App__ConnectionString=Host=db;Port=5432;Database=notification_service;Username=postgres;Password=postgres - App__ConnectionString=Host=db;Port=5432;Database=notification_service;Username=postgres;Password=postgres
api: web:
environment: environment:
- ASPNETCORE_ENVIRONMENT=Development - ASPNETCORE_ENVIRONMENT=Development
- App__ConnectionString=Host=db;Port=5432;Database=notification_service;Username=postgres;Password=postgres - App__ConnectionString=Host=db;Port=5432;Database=notification_service;Username=postgres;Password=postgres
@@ -26,31 +26,22 @@ services:
condition: service_healthy condition: service_healthy
rabbitmq: rabbitmq:
image: rabbitmq:4-management-alpine
environment: environment:
RABBITMQ_DEFAULT_USER: guest RABBITMQ_DEFAULT_USER: guest
RABBITMQ_DEFAULT_PASS: guest RABBITMQ_DEFAULT_PASS: guest
ports: ports:
- "5672:5672" - "5672:5672"
- "15672:15672" - "15672:15672"
healthcheck: networks:
test: ["CMD", "rabbitmq-diagnostics", "ping"] - internal
interval: 10s
timeout: 5s
retries: 5
volumes:
- notification_rabbitmq:/var/lib/rabbitmq
db: db:
image: postgres:17
environment:
POSTGRES_DB: notification_service
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
ports: ports:
- "5433:5432" - "5433:5432"
volumes: volumes:
- notification_db:/var/lib/postgresql/data - pgdata:/var/lib/postgresql/data
networks:
- internal
seq: seq:
image: datalust/seq:2024 image: datalust/seq:2024
@@ -60,9 +51,14 @@ services:
ports: ports:
- "5342:80" - "5342:80"
volumes: volumes:
- notification_seq:/data - seq_data:/data
networks:
- internal
volumes: volumes:
notification_db: pgdata:
notification_seq: name: ns-dev-pgdata
notification_rabbitmq: rabbitmq_data:
name: ns-dev-rabbitmq-data
seq_data:
name: ns-dev-seq
@@ -0,0 +1,37 @@
services:
migrator:
build: {}
image: registry.grynco.com.ua/hrynco.notification-service.migrator:${MIGRATOR_IMAGE_TAG:?MIGRATOR_IMAGE_TAG is required}
web:
build: {}
image: registry.grynco.com.ua/hrynco.notification-service.web:${WEB_IMAGE_TAG:?WEB_IMAGE_TAG is required}
ports:
- "${WEB_PORT:?WEB_PORT is required}:8080"
environment:
- Serilog__WriteTo__1__Args__serverUrl=${SEQ_URL:-}
restart: always
worker:
build: {}
image: registry.grynco.com.ua/hrynco.notification-service.worker:${WORKER_IMAGE_TAG:?WORKER_IMAGE_TAG is required}
environment:
- Serilog__WriteTo__1__Args__serverUrl=${SEQ_URL:-}
restart: always
rabbitmq:
restart: always
networks:
- internal
- hrynco-services
db:
ports:
- "${DB_PORT:?DB_PORT is required}:5432"
restart: always
networks:
internal: {}
hrynco-services:
external: true
name: hrynco-services
+51 -11
View File
@@ -6,24 +6,28 @@ services:
context: ../.. context: ../..
dockerfile: HrynCo.NotificationService.Migrator/Dockerfile dockerfile: HrynCo.NotificationService.Migrator/Dockerfile
environment: environment:
- App__ConnectionString=${CONNECTION_STRING} - App__ConnectionString=Host=db;Port=5432;Database=${DB_NAME:?DB_NAME is required};Username=${DB_USER:?DB_USER is required};Password=${DB_PASS:?DB_PASS is required}
depends_on: depends_on:
db: db:
condition: service_started condition: service_healthy
networks:
- internal
restart: "no" restart: "no"
api: web:
build: build:
context: ../.. context: ../..
dockerfile: HrynCo.NotificationService.Web/Dockerfile dockerfile: HrynCo.NotificationService.Web/Dockerfile
environment: environment:
- ASPNETCORE_ENVIRONMENT=Production - ASPNETCORE_ENVIRONMENT=Production
- App__ConnectionString=${CONNECTION_STRING} - App__ConnectionString=Host=db;Port=5432;Database=${DB_NAME:?DB_NAME is required};Username=${DB_USER:?DB_USER is required};Password=${DB_PASS:?DB_PASS is required}
depends_on: depends_on:
db: db:
condition: service_started condition: service_healthy
migrator: migrator:
condition: service_completed_successfully condition: service_completed_successfully
networks:
- internal
worker: worker:
build: build:
@@ -31,25 +35,61 @@ services:
dockerfile: HrynCo.NotificationService.Worker/Dockerfile dockerfile: HrynCo.NotificationService.Worker/Dockerfile
environment: environment:
- DOTNET_ENVIRONMENT=Production - DOTNET_ENVIRONMENT=Production
- App__ConnectionString=${CONNECTION_STRING} - App__ConnectionString=Host=db;Port=5432;Database=${DB_NAME:?DB_NAME is required};Username=${DB_USER:?DB_USER is required};Password=${DB_PASS:?DB_PASS is required}
- App__RabbitMq__Host=rabbitmq - App__RabbitMq__Host=rabbitmq
- App__RabbitMq__User=${RABBITMQ_USER:-guest} - App__RabbitMq__Port=5672
- App__RabbitMq__Password=${RABBITMQ_PASSWORD:-guest} - App__RabbitMq__User=${RABBITMQ_USER:?RABBITMQ_USER is required}
- App__RabbitMq__Password=${RABBITMQ_PASSWORD:?RABBITMQ_PASSWORD is required}
depends_on: depends_on:
db: db:
condition: service_started condition: service_healthy
migrator: migrator:
condition: service_completed_successfully condition: service_completed_successfully
rabbitmq: rabbitmq:
condition: service_healthy condition: service_healthy
networks:
- internal
rabbitmq: rabbitmq:
image: rabbitmq:4-management-alpine image: rabbitmq:4-management-alpine
environment: environment:
RABBITMQ_DEFAULT_USER: ${RABBITMQ_USER:-guest} RABBITMQ_DEFAULT_USER: ${RABBITMQ_USER:?RABBITMQ_USER is required}
RABBITMQ_DEFAULT_PASS: ${RABBITMQ_PASSWORD:-guest} RABBITMQ_DEFAULT_PASS: ${RABBITMQ_PASSWORD:?RABBITMQ_PASSWORD is required}
ports:
- "${RABBITMQ_AMQP_PORT:?RABBITMQ_AMQP_PORT is required}:5672"
- "${RABBITMQ_MANAGEMENT_PORT:?RABBITMQ_MANAGEMENT_PORT is required}:15672"
volumes:
- rabbitmq_data:/var/lib/rabbitmq
networks:
- internal
healthcheck: healthcheck:
test: ["CMD", "rabbitmq-diagnostics", "ping"] test: ["CMD", "rabbitmq-diagnostics", "ping"]
interval: 10s interval: 10s
timeout: 5s timeout: 5s
retries: 5 retries: 5
db:
image: postgres:17
environment:
- POSTGRES_DB=${DB_NAME:?DB_NAME is required}
- POSTGRES_USER=${DB_USER:?DB_USER is required}
- POSTGRES_PASSWORD=${DB_PASS:?DB_PASS is required}
volumes:
- pgdata:/var/lib/postgresql/data
networks:
- internal
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${DB_USER}"]
interval: 5s
timeout: 5s
retries: 10
volumes:
pgdata:
name: ${VOLUME_PREFIX:?VOLUME_PREFIX is required}-pgdata
rabbitmq_data:
name: ${VOLUME_PREFIX:?VOLUME_PREFIX is required}-rabbitmq-data
networks:
internal:
driver: bridge