diff --git a/Streetcode/Streetcode.BLL/DTO/Email/EmailDTO.cs b/Streetcode/Streetcode.BLL/DTO/Email/EmailDTO.cs index 8635ee1..137074b 100644 --- a/Streetcode/Streetcode.BLL/DTO/Email/EmailDTO.cs +++ b/Streetcode/Streetcode.BLL/DTO/Email/EmailDTO.cs @@ -1,9 +1,13 @@ -using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations; namespace Streetcode.BLL.DTO.Email { public class EmailDTO { + public EmailDTO() + { + } + [MaxLength(80)] public string From { get; set; } @@ -11,4 +15,4 @@ public class EmailDTO [StringLength(500, MinimumLength = 1)] public string Content { get; set; } } -} +} \ No newline at end of file diff --git a/Streetcode/Streetcode.BLL/Interfaces/Email/IEmailService.cs b/Streetcode/Streetcode.BLL/Interfaces/Email/IEmailService.cs deleted file mode 100644 index 7817456..0000000 --- a/Streetcode/Streetcode.BLL/Interfaces/Email/IEmailService.cs +++ /dev/null @@ -1,9 +0,0 @@ -using Streetcode.DAL.Entities.AdditionalContent.Email; - -namespace Streetcode.BLL.Interfaces.Email -{ - public interface IEmailService - { - Task SendEmailAsync(Message message); - } -} diff --git a/Streetcode/Streetcode.BLL/MediatR/Email/SendEmailCommand.cs b/Streetcode/Streetcode.BLL/MediatR/Email/SendEmailCommand.cs deleted file mode 100644 index 7a7d1da..0000000 --- a/Streetcode/Streetcode.BLL/MediatR/Email/SendEmailCommand.cs +++ /dev/null @@ -1,6 +0,0 @@ -using FluentResults; -using MediatR; -using Streetcode.BLL.DTO.Email; - -namespace Streetcode.BLL.MediatR.Email; -public record SendEmailCommand(EmailDTO Email) : IRequest>; diff --git a/Streetcode/Streetcode.BLL/MediatR/Email/SendEmailHandler.cs b/Streetcode/Streetcode.BLL/MediatR/Email/SendEmailHandler.cs deleted file mode 100644 index c16e438..0000000 --- a/Streetcode/Streetcode.BLL/MediatR/Email/SendEmailHandler.cs +++ /dev/null @@ -1,42 +0,0 @@ -using FluentResults; -using MediatR; -using Streetcode.BLL.Interfaces.Email; -using Streetcode.BLL.Interfaces.Logging; -using Streetcode.DAL.Entities.AdditionalContent.Email; -using Streetcode.Resources; -using Streetcode.Shared; - -namespace Streetcode.BLL.MediatR.Email -{ - public class SendEmailHandler : IRequestHandler> - { - private readonly IEmailService _emailService; - private readonly ILoggerService _logger; - - public SendEmailHandler(IEmailService emailService, ILoggerService logger) - { - _emailService = emailService; - _logger = logger; - } - - public async Task> Handle(SendEmailCommand request, CancellationToken cancellationToken) - { - var message = new Message( - [Constants.StreetcodeContacts.Email], - request.Email.From, - "FeedBack", - request.Email.Content); - - var isResultSuccess = await _emailService.SendEmailAsync(message); - - if (isResultSuccess) - { - return Result.Ok(Unit.Value); - } - - var errorMsg = Messages.Error_FailedToSendEmail; - _logger.LogError(request, errorMsg); - return Result.Fail(new Error(errorMsg)); - } - } -} diff --git a/Streetcode/Streetcode.BLL/Services/Email/EmailService.cs b/Streetcode/Streetcode.BLL/Services/Email/EmailService.cs deleted file mode 100644 index 0c3d093..0000000 --- a/Streetcode/Streetcode.BLL/Services/Email/EmailService.cs +++ /dev/null @@ -1,70 +0,0 @@ -using MailKit.Net.Smtp; -using MimeKit; -using Streetcode.BLL.Interfaces.Email; -using Streetcode.DAL.Entities.AdditionalContent.Email; - -namespace Streetcode.BLL.Services.Email -{ - public class EmailService : IEmailService - { - private readonly EmailConfiguration _emailConfig; - - public EmailService(EmailConfiguration emailConfig) - { - _emailConfig = emailConfig; - } - - public async Task SendEmailAsync(Message message) - { - var mailMessage = CreateEmailMessage(message); - - return await SendAsync(mailMessage); - } - - private MimeMessage CreateEmailMessage(Message message) - { - var emailMessage = new MimeMessage(); - emailMessage.From.Add(new MailboxAddress("", _emailConfig.From)); - emailMessage.To.AddRange(message.To); - emailMessage.Subject = message.Subject; - - var bodyBuilder = new BodyBuilder - { - HtmlBody = - "

" + - $"Від: {message.From}
" + - $"Текст: {message.Content}" + - "

" - }; - - emailMessage.Body = bodyBuilder.ToMessageBody(); - return emailMessage; - } - - private async Task SendAsync(MimeMessage mailMessage) - { - using (var client = new SmtpClient()) - { - try - { - await client.ConnectAsync(_emailConfig.SmtpServer, _emailConfig.Port, true); - client.AuthenticationMechanisms.Remove("XOAUTH2"); - await client.AuthenticateAsync(_emailConfig.UserName, _emailConfig.Password); - - await client.SendAsync(mailMessage); - return true; - } - catch - { - // Logger - return false; - } - finally - { - await client.DisconnectAsync(true); - client.Dispose(); - } - } - } - } -} diff --git a/Streetcode/Streetcode.Email.BLL/Configs/EmailConfiguration.cs b/Streetcode/Streetcode.Email.BLL/Configs/EmailConfiguration.cs new file mode 100644 index 0000000..de289d4 --- /dev/null +++ b/Streetcode/Streetcode.Email.BLL/Configs/EmailConfiguration.cs @@ -0,0 +1,14 @@ +namespace Streetcode.Email.BLL.Configs +{ + public class EmailConfiguration + { + public const string SectionName = "EmailConfiguration"; + + public string FromAddress { get; set; } + public string AdminAddress { get; set; } + public string SmtpServer { get; set; } + public int Port { get; set; } + public string SmtpUser { get; set; } + public string SmtpPassword { get; set; } + } +} diff --git a/Streetcode/Streetcode.Email.BLL/DTO/EmailDTO.cs b/Streetcode/Streetcode.Email.BLL/DTO/EmailDTO.cs new file mode 100644 index 0000000..dfd14e3 --- /dev/null +++ b/Streetcode/Streetcode.Email.BLL/DTO/EmailDTO.cs @@ -0,0 +1,11 @@ +namespace Streetcode.Email.BLL.DTO +{ + public class EmailDTO + { + public EmailDTO() + { + } + public string From { get; set; } + public string Content { get; set; } + } +} diff --git a/Streetcode/Streetcode.Email.BLL/Exceptions/ValidationException.cs b/Streetcode/Streetcode.Email.BLL/Exceptions/ValidationException.cs new file mode 100644 index 0000000..d2c6dd1 --- /dev/null +++ b/Streetcode/Streetcode.Email.BLL/Exceptions/ValidationException.cs @@ -0,0 +1,29 @@ +using FluentValidation.Results; + +namespace Streetcode.Email.BLL.Exceptions +{ + public class ValidationException : Exception + { + public ValidationException() : base("One or more validation failures have occurred.") + { + Errors = new Dictionary(); + } + + public ValidationException(IEnumerable failures) : this() + { + Errors = failures + .GroupBy(e => e.PropertyName, e => e.ErrorMessage) + .ToDictionary(failureGroup => failureGroup.Key, failureGroup => failureGroup.ToArray()); + } + + public ValidationException(string propertyName, string errorMessage) : this() + { + Errors = new Dictionary + { + { propertyName, new[] { errorMessage } } + }; + } + + public IDictionary Errors { get; } + } +} \ No newline at end of file diff --git a/Streetcode/Streetcode.Email.BLL/Interfaces/IEmailService.cs b/Streetcode/Streetcode.Email.BLL/Interfaces/IEmailService.cs new file mode 100644 index 0000000..9582664 --- /dev/null +++ b/Streetcode/Streetcode.Email.BLL/Interfaces/IEmailService.cs @@ -0,0 +1,9 @@ +using Streetcode.Email.BLL.DTO; + +namespace Streetcode.Email.BLL.Interfaces +{ + public interface IEmailService + { + Task SendEmailAsync(EmailDTO email); + } +} \ No newline at end of file diff --git a/Streetcode/Streetcode.Email.BLL/Mapping/EmailProfile.cs b/Streetcode/Streetcode.Email.BLL/Mapping/EmailProfile.cs new file mode 100644 index 0000000..7bc2a7f --- /dev/null +++ b/Streetcode/Streetcode.Email.BLL/Mapping/EmailProfile.cs @@ -0,0 +1,14 @@ +using AutoMapper; +using Streetcode.Email.BLL.DTO; +using EmailEntity = Streetcode.Email.DAL.Entities.Email; + +namespace Streetcode.Email.BLL.Mapping +{ + public class EmailProfile : Profile + { + public EmailProfile() + { + CreateMap().ReverseMap(); + } + } +} diff --git a/Streetcode/Streetcode.Email.BLL/MediatR/Behavior/ValidatorBehavior.cs b/Streetcode/Streetcode.Email.BLL/MediatR/Behavior/ValidatorBehavior.cs new file mode 100644 index 0000000..36213eb --- /dev/null +++ b/Streetcode/Streetcode.Email.BLL/MediatR/Behavior/ValidatorBehavior.cs @@ -0,0 +1,33 @@ +using FluentValidation; +using MediatR; + +namespace Streetcode.Email.BLL.MediatR.Behavior +{ + public class ValidatorBehavior : IPipelineBehavior + where TRequest : IRequest + { + private readonly IEnumerable> _validators; + + public ValidatorBehavior(IEnumerable> validators) + { + _validators = validators; + } + + public Task Handle(TRequest request, RequestHandlerDelegate next, CancellationToken cancellationToken) + { + var context = new ValidationContext(request); + var failures = _validators + .Select(v => v.Validate(context)) + .SelectMany(result => result.Errors) + .Where(f => f != null) + .ToList(); + + if (failures.Count != 0) + { + throw new Exceptions.ValidationException(failures); + } + + return next(); + } + } +} diff --git a/Streetcode/Streetcode.Email.BLL/MediatR/Email/EmailDTOValidator.cs b/Streetcode/Streetcode.Email.BLL/MediatR/Email/EmailDTOValidator.cs new file mode 100644 index 0000000..5ccf163 --- /dev/null +++ b/Streetcode/Streetcode.Email.BLL/MediatR/Email/EmailDTOValidator.cs @@ -0,0 +1,20 @@ +using FluentValidation; +using Streetcode.Email.BLL.DTO; + +namespace Streetcode.Email.BLL.MediatR.Email +{ + public class EmailDTOValidator : AbstractValidator + { + public EmailDTOValidator() + { + RuleFor(x => x.From) + .NotEmpty() + .EmailAddress(); + + RuleFor(x => x.Content) + .NotEmpty() + .MinimumLength(5) + .MaximumLength(1000); + } + } +} diff --git a/Streetcode/Streetcode.Email.BLL/MediatR/Email/SendEmailCommand.cs b/Streetcode/Streetcode.Email.BLL/MediatR/Email/SendEmailCommand.cs new file mode 100644 index 0000000..98a028a --- /dev/null +++ b/Streetcode/Streetcode.Email.BLL/MediatR/Email/SendEmailCommand.cs @@ -0,0 +1,8 @@ +using FluentResults; +using MediatR; +using Streetcode.Email.BLL.DTO; + +namespace Streetcode.Email.BLL.MediatR.Email +{ + public record SendEmailCommand(EmailDTO email) : IRequest>; +} diff --git a/Streetcode/Streetcode.Email.BLL/MediatR/Email/SendEmailCommandValidator.cs b/Streetcode/Streetcode.Email.BLL/MediatR/Email/SendEmailCommandValidator.cs new file mode 100644 index 0000000..596df14 --- /dev/null +++ b/Streetcode/Streetcode.Email.BLL/MediatR/Email/SendEmailCommandValidator.cs @@ -0,0 +1,16 @@ +using FluentValidation; +using Streetcode.Resources; + +namespace Streetcode.Email.BLL.MediatR.Email +{ + public class SendEmailCommandValidator : AbstractValidator + { + public SendEmailCommandValidator() + { + RuleFor(x => x.email) + .NotNull() + .WithMessage(Messages.Error_CommandDataRequired) + .SetValidator(new EmailDTOValidator()); + } + } +} diff --git a/Streetcode/Streetcode.Email.BLL/MediatR/Email/SendEmailHandler.cs b/Streetcode/Streetcode.Email.BLL/MediatR/Email/SendEmailHandler.cs new file mode 100644 index 0000000..76ae843 --- /dev/null +++ b/Streetcode/Streetcode.Email.BLL/MediatR/Email/SendEmailHandler.cs @@ -0,0 +1,52 @@ +using AutoMapper; +using FluentResults; +using Hangfire; +using MediatR; +using Microsoft.Extensions.Logging; +using Streetcode.Email.BLL.DTO; +using Streetcode.Email.BLL.Interfaces; +using Streetcode.Email.DAL.Persistence; +using Streetcode.Resources; +using EmailEntity = Streetcode.Email.DAL.Entities.Email; + +namespace Streetcode.Email.BLL.MediatR.Email +{ + public class SendEmailHandler : IRequestHandler> + { + private readonly EmailDbContext _context; + private readonly IMapper _mapper; + private readonly ILogger _logger; + private readonly IBackgroundJobClient _backgroundJob; + + public SendEmailHandler(EmailDbContext context, IMapper mapper, ILogger logger, IBackgroundJobClient backgroundJob) + { + _context = context; + _mapper = mapper; + _logger = logger; + _backgroundJob = backgroundJob; + } + + public async Task> Handle(SendEmailCommand request, CancellationToken cancellationToken) + { + var EmailEntity = _mapper.Map(request.email); + + _context.Emails.Add(EmailEntity); + + var rowsAffected = await _context.SaveChangesAsync(cancellationToken); + + if (rowsAffected <= 0) + { + var errorMsg = Messages.Error_FailedToCreateEntity; + _logger.LogError(errorMsg); + return Result.Fail(errorMsg); + } + + _backgroundJob.Enqueue(emailService => + emailService.SendEmailAsync(request.email)); + + _logger.LogInformation("Email saved to DB and email task enqueued for {Email}", request.email.From); + + return Result.Ok(Unit.Value); + } + } +} diff --git a/Streetcode/Streetcode.Email.BLL/Services/EmailConsumer.cs b/Streetcode/Streetcode.Email.BLL/Services/EmailConsumer.cs new file mode 100644 index 0000000..7fe53db --- /dev/null +++ b/Streetcode/Streetcode.Email.BLL/Services/EmailConsumer.cs @@ -0,0 +1,33 @@ +using MassTransit; +using MediatR; +using Streetcode.Email.BLL.DTO; +using Streetcode.Email.BLL.MediatR.Email; +using Streetcode.Shared.Contracts; + +namespace Streetcode.Email.BLL.Services +{ + public class EmailConsumer : IConsumer + { + private readonly IMediator _mediator; + public EmailConsumer(IMediator mediator) + { + _mediator = mediator; + } + + public async Task Consume(ConsumeContext context) + { + var message = context.Message; + + var email = new EmailDTO + { + From = message.From, + Content = message.Content + }; + + var command = new SendEmailCommand(email); + + await _mediator.Send(command); + } + } +} + diff --git a/Streetcode/Streetcode.Email.BLL/Services/EmailService.cs b/Streetcode/Streetcode.Email.BLL/Services/EmailService.cs new file mode 100644 index 0000000..92ed4d1 --- /dev/null +++ b/Streetcode/Streetcode.Email.BLL/Services/EmailService.cs @@ -0,0 +1,55 @@ +using MailKit.Net.Smtp; +using MailKit.Security; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using MimeKit; +using Streetcode.Email.BLL.Configs; +using Streetcode.Email.BLL.DTO; +using Streetcode.Email.BLL.Interfaces; +using System.Data; + +namespace Streetcode.Email.BLL.Services +{ + public class EmailService : IEmailService + { + private readonly EmailConfiguration _emailConfig; + private readonly ILogger _logger; + + public EmailService(IOptions options, ILogger logger) + { + _emailConfig = options.Value; + _logger = logger; + } + + public async Task SendEmailAsync(EmailDTO email) + { + try + { + var message = new MimeMessage(); + message.From.Add(new MailboxAddress("Streetcode", _emailConfig.FromAddress)); + message.To.Add(new MailboxAddress("Streetcode Admin", _emailConfig.AdminAddress)); + message.Subject = $"New feedback from Streetcode User"; + message.Body = new TextPart("plain") + { + Text = $"Користувач {email.From} залишив повідомлення:\n\n{email.Content}" + }; + + using var client = new SmtpClient(); + + await client.ConnectAsync(_emailConfig.SmtpServer, _emailConfig.Port, SecureSocketOptions.Auto); + await client.AuthenticateAsync(_emailConfig.SmtpUser, _emailConfig.SmtpPassword); + + await client.SendAsync(message); + await client.DisconnectAsync(true); + + _logger.LogInformation("Email sent successfully to {Recipient} from {Sender}.", _emailConfig.AdminAddress, email.From); + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to send email to {Recipient} due to an error.", _emailConfig.AdminAddress); + + throw; + } + } + } +} diff --git a/Streetcode/Streetcode.Email.BLL/Streetcode.Email.BLL.csproj b/Streetcode/Streetcode.Email.BLL/Streetcode.Email.BLL.csproj new file mode 100644 index 0000000..10da8bc --- /dev/null +++ b/Streetcode/Streetcode.Email.BLL/Streetcode.Email.BLL.csproj @@ -0,0 +1,28 @@ + + + + net8.0 + enable + enable + + + + + + + + + + + + + + + + + + + + + + diff --git a/Streetcode/Streetcode.Email.DAL/Entities/Email.cs b/Streetcode/Streetcode.Email.DAL/Entities/Email.cs new file mode 100644 index 0000000..3f6920f --- /dev/null +++ b/Streetcode/Streetcode.Email.DAL/Entities/Email.cs @@ -0,0 +1,19 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace Streetcode.Email.DAL.Entities; + +[Table("emails", Schema = "email")] +public class Email + { + [Key] + public int Id { get; set; } + [Required] + [EmailAddress] + public string? From { get; set; } + [Required] + [MinLength(5)] + [MaxLength(1000)] + public string? Content { get; set; } + } + diff --git a/Streetcode/Streetcode.Email.DAL/Migrations/20260227122516_Name.Designer.cs b/Streetcode/Streetcode.Email.DAL/Migrations/20260227122516_Name.Designer.cs new file mode 100644 index 0000000..ede2f6a --- /dev/null +++ b/Streetcode/Streetcode.Email.DAL/Migrations/20260227122516_Name.Designer.cs @@ -0,0 +1,52 @@ +// +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Streetcode.Email.DAL.Persistence; + +#nullable disable + +namespace Streetcode.Email.DAL.Migrations +{ + [DbContext(typeof(EmailDbContext))] + [Migration("20260227122516_Name")] + partial class Name + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.23") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("Streetcode.Email.DAL.Entities.Feedback", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Email") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("Message") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.HasKey("Id"); + + b.ToTable("Feedbacks", (string)null); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Streetcode/Streetcode.Email.DAL/Migrations/20260227122516_Name.cs b/Streetcode/Streetcode.Email.DAL/Migrations/20260227122516_Name.cs new file mode 100644 index 0000000..2ec9e22 --- /dev/null +++ b/Streetcode/Streetcode.Email.DAL/Migrations/20260227122516_Name.cs @@ -0,0 +1,35 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Streetcode.Email.DAL.Migrations +{ + /// + public partial class Name : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "Feedbacks", + columns: table => new + { + Id = table.Column(type: "int", nullable: false) + .Annotation("SqlServer:Identity", "1, 1"), + Email = table.Column(type: "nvarchar(256)", maxLength: 256, nullable: false), + Message = table.Column(type: "nvarchar(100)", maxLength: 100, nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Feedbacks", x => x.Id); + }); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "Feedbacks"); + } + } +} diff --git a/Streetcode/Streetcode.Email.DAL/Migrations/20260302112047_RenameFeedbackIntoEmail.Designer.cs b/Streetcode/Streetcode.Email.DAL/Migrations/20260302112047_RenameFeedbackIntoEmail.Designer.cs new file mode 100644 index 0000000..9d2b1b6 --- /dev/null +++ b/Streetcode/Streetcode.Email.DAL/Migrations/20260302112047_RenameFeedbackIntoEmail.Designer.cs @@ -0,0 +1,52 @@ +// +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Streetcode.Email.DAL.Persistence; + +#nullable disable + +namespace Streetcode.Email.DAL.Migrations +{ + [DbContext(typeof(EmailDbContext))] + [Migration("20260302112047_RenameFeedbackIntoEmail")] + partial class RenameFeedbackIntoEmail + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.23") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("Streetcode.Email.DAL.Entities.Email", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Content") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("From") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.HasKey("Id"); + + b.ToTable("Emails", (string)null); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Streetcode/Streetcode.Email.DAL/Migrations/20260302112047_RenameFeedbackIntoEmail.cs b/Streetcode/Streetcode.Email.DAL/Migrations/20260302112047_RenameFeedbackIntoEmail.cs new file mode 100644 index 0000000..331f7ce --- /dev/null +++ b/Streetcode/Streetcode.Email.DAL/Migrations/20260302112047_RenameFeedbackIntoEmail.cs @@ -0,0 +1,66 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Streetcode.Email.DAL.Migrations +{ + /// + public partial class RenameFeedbackIntoEmail : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.RenameTable( + name: "Feedbacks", + newName: "Emails"); + + migrationBuilder.RenameColumn( + name: "Email", + table: "Emails", + newName: "From"); + + migrationBuilder.RenameColumn( + name: "Message", + table: "Emails", + newName: "Content"); + + migrationBuilder.AlterColumn( + name: "Content", + table: "Emails", + type: "nvarchar(1000)", + maxLength: 1000, + nullable: false, + oldClrType: typeof(string), + oldType: "nvarchar(100)", + oldMaxLength: 100); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterColumn( + name: "Content", + table: "Emails", + type: "nvarchar(100)", + maxLength: 100, + nullable: false, + oldClrType: typeof(string), + oldType: "nvarchar(1000)", + oldMaxLength: 1000); + + migrationBuilder.RenameTable( + name: "Emails", + newName: "Feedbacks"); + + migrationBuilder.RenameColumn( + name: "From", + table: "Feedbacks", + newName: "Email"); + + migrationBuilder.RenameColumn( + name: "Content", + table: "Feedbacks", + newName: "Message"); + } + } +} diff --git a/Streetcode/Streetcode.Email.DAL/Migrations/EmailDbContextModelSnapshot.cs b/Streetcode/Streetcode.Email.DAL/Migrations/EmailDbContextModelSnapshot.cs new file mode 100644 index 0000000..c8de553 --- /dev/null +++ b/Streetcode/Streetcode.Email.DAL/Migrations/EmailDbContextModelSnapshot.cs @@ -0,0 +1,49 @@ +// +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Streetcode.Email.DAL.Persistence; + +#nullable disable + +namespace Streetcode.Email.DAL.Migrations +{ + [DbContext(typeof(EmailDbContext))] + partial class EmailDbContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.23") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("Streetcode.Email.DAL.Entities.Email", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Content") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("From") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.HasKey("Id"); + + b.ToTable("Emails", (string)null); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Streetcode/Streetcode.Email.DAL/Persistence/EmailDbContext.cs b/Streetcode/Streetcode.Email.DAL/Persistence/EmailDbContext.cs new file mode 100644 index 0000000..bbc7001 --- /dev/null +++ b/Streetcode/Streetcode.Email.DAL/Persistence/EmailDbContext.cs @@ -0,0 +1,28 @@ +using Microsoft.EntityFrameworkCore; +using EmailEntity = Streetcode.Email.DAL.Entities.Email; + +namespace Streetcode.Email.DAL.Persistence +{ + public class EmailDbContext : DbContext + { + public EmailDbContext(DbContextOptions options) + : base(options) + { + } + + public DbSet Emails { get; set; } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + + modelBuilder.Entity(entity => + { + entity.ToTable("Emails"); + entity.HasKey(e => e.Id); + entity.Property(e => e.From).IsRequired().HasMaxLength(256); + entity.Property(e => e.Content).IsRequired(); + }); + } + } +} diff --git a/Streetcode/Streetcode.Email.DAL/Streetcode.Email.DAL.csproj b/Streetcode/Streetcode.Email.DAL/Streetcode.Email.DAL.csproj new file mode 100644 index 0000000..51b57ae --- /dev/null +++ b/Streetcode/Streetcode.Email.DAL/Streetcode.Email.DAL.csproj @@ -0,0 +1,22 @@ + + + + net8.0 + enable + enable + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + diff --git a/Streetcode/Streetcode.Email.WebAPI/Extensions/ServiceCollectionExtensions.cs b/Streetcode/Streetcode.Email.WebAPI/Extensions/ServiceCollectionExtensions.cs new file mode 100644 index 0000000..356e31b --- /dev/null +++ b/Streetcode/Streetcode.Email.WebAPI/Extensions/ServiceCollectionExtensions.cs @@ -0,0 +1,91 @@ +using FluentValidation; +using Hangfire; +using Hangfire.SqlServer; +using MassTransit; +using MediatR; +using Microsoft.EntityFrameworkCore; +using Streetcode.Email.BLL.Configs; +using Streetcode.Email.BLL.Interfaces; +using Streetcode.Email.BLL.MediatR.Behavior; +using Streetcode.Email.BLL.Services; +using Streetcode.Email.DAL.Persistence; +using System.Reflection; + +namespace Streetcode.Email.WebAPI.Extensions +{ + public static class ServiceCollectionExtensions + { + public static void AddApplicationServices(this IServiceCollection services, ConfigurationManager configuration) + { + var connectionString = configuration.GetConnectionString("DefaultConnection"); + + services.AddDbContext(options => + { + options.UseSqlServer(connectionString, opt => + { + opt.MigrationsAssembly(typeof(EmailDbContext).Assembly.GetName().Name); + opt.MigrationsHistoryTable("__EFMigrationsHistory", schema: "entity_framework"); + }); + }); + + services.AddHangfire(config => config + .SetDataCompatibilityLevel(CompatibilityLevel.Version_180) + .UseSimpleAssemblyNameTypeSerializer() + .UseRecommendedSerializerSettings() + .UseSqlServerStorage(connectionString, new SqlServerStorageOptions + { + PrepareSchemaIfNecessary = true, + + CommandBatchMaxTimeout = TimeSpan.FromMinutes(5), + SlidingInvisibilityTimeout = TimeSpan.FromMinutes(5), + QueuePollInterval = TimeSpan.Zero, + UseRecommendedIsolationLevel = true, + DisableGlobalLocks = true + })); + + services.AddHangfireServer(); + + services.AddLogging(); + + services.AddControllers(); + } + public static void AddCustomServices(this IServiceCollection services, IConfiguration configuration) + { + var bllAssembly = Assembly.Load("Streetcode.Email.BLL"); + + services.AddAutoMapper(bllAssembly); + services.AddMediatR(cfg => cfg.RegisterServicesFromAssemblies(bllAssembly)); + services.Configure( + configuration.GetSection("EmailConfiguration")); + + services.AddScoped(); + + services.AddValidatorsFromAssembly(bllAssembly); + services.AddTransient(typeof(IPipelineBehavior<,>), typeof(ValidatorBehavior<,>)); + + services.AddMassTransit(x => + { + x.AddConsumer(); + + x.UsingRabbitMq((context, cfg) => + { + var rabbitSection = configuration.GetSection("RabbitMQ"); + + var host = rabbitSection["Host"] + ?? throw new InvalidOperationException("RabbitMQ Host is missing"); + var username = rabbitSection["Username"] + ?? throw new InvalidOperationException("RabbitMQ Username is missing"); + var password = rabbitSection["Password"] + ?? throw new InvalidOperationException("RabbitMQ Password is missing"); + + cfg.Host(host, "/", h => + { + h.Username(username); + h.Password(password); + }); + cfg.ConfigureEndpoints(context); + }); + }); + } + } +} \ No newline at end of file diff --git a/Streetcode/Streetcode.Email.WebAPI/Extensions/WebApplicationExtensions.cs b/Streetcode/Streetcode.Email.WebAPI/Extensions/WebApplicationExtensions.cs new file mode 100644 index 0000000..c47d63c --- /dev/null +++ b/Streetcode/Streetcode.Email.WebAPI/Extensions/WebApplicationExtensions.cs @@ -0,0 +1,27 @@ +using Microsoft.EntityFrameworkCore; +using Streetcode.Email.DAL.Persistence; + +namespace Streetcode.Email.WebAPI.Extensions +{ + public static class WebApplicationExtensions + { + public static async Task ApplyMigrations(this WebApplication app) + { + using var scope = app.Services.CreateScope(); + var services = scope.ServiceProvider; + var logger = services.GetRequiredService>(); + + try + { + var emailContext = services.GetRequiredService(); + await emailContext.Database.MigrateAsync(); + logger.LogInformation("Database migrated successfully."); + } + catch (Exception ex) + { + logger.LogError(ex, "An error occurred during startup migration"); + throw; + } + } + } +} diff --git a/Streetcode/Streetcode.Email.WebAPI/Program.cs b/Streetcode/Streetcode.Email.WebAPI/Program.cs new file mode 100644 index 0000000..9a9684b --- /dev/null +++ b/Streetcode/Streetcode.Email.WebAPI/Program.cs @@ -0,0 +1,27 @@ +using Hangfire; +using Streetcode.Email.BLL.Interfaces; +using Streetcode.Email.WebAPI.Extensions; + +var builder = WebApplication.CreateBuilder(args); + +builder.Services.AddApplicationServices(builder.Configuration); +builder.Services.AddCustomServices(builder.Configuration); + +var app = builder.Build(); + +if (app.Environment.IsDevelopment() || app.Environment.EnvironmentName == "Local") +{ + app.UseHangfireDashboard("/dash"); +} +else +{ + app.UseHsts(); +} + +await app.ApplyMigrations(); + +// app.SeedDataAsync(); // uncomment for seeding data in local + +app.MapControllers(); + +app.Run(); diff --git a/Streetcode/Streetcode.Email.WebAPI/Properties/launchSettings.json b/Streetcode/Streetcode.Email.WebAPI/Properties/launchSettings.json new file mode 100644 index 0000000..d554bda --- /dev/null +++ b/Streetcode/Streetcode.Email.WebAPI/Properties/launchSettings.json @@ -0,0 +1,41 @@ +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:48956", + "sslPort": 44318 + } + }, + "profiles": { + "Streetcode_Email_Local": { + "commandName": "Project", + "launchBrowser": false, + "environmentVariables": { + "STREETCODE_ENVIRONMENT": "Local", + "ASPNETCORE_ENVIRONMENT": "Local" + }, + "dotnetRunMessages": true, + "applicationUrl": "https://localhost:7094;http://localhost:5179" + }, + "Streetcode_Email_Dev": { + "commandName": "Project", + "launchBrowser": false, + "launchUrl": "swagger", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "dotnetRunMessages": true, + "applicationUrl": "https://localhost:7094;http://localhost:5179" + }, + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "launchUrl": "swagger", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/Streetcode/Streetcode.Email.WebAPI/Streetcode.Email.WebAPI.csproj b/Streetcode/Streetcode.Email.WebAPI/Streetcode.Email.WebAPI.csproj new file mode 100644 index 0000000..a29f5f4 --- /dev/null +++ b/Streetcode/Streetcode.Email.WebAPI/Streetcode.Email.WebAPI.csproj @@ -0,0 +1,32 @@ + + + + net8.0 + enable + enable + + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + + + diff --git a/Streetcode/Streetcode.Email.WebAPI/appsettings.json b/Streetcode/Streetcode.Email.WebAPI/appsettings.json new file mode 100644 index 0000000..c451cd8 --- /dev/null +++ b/Streetcode/Streetcode.Email.WebAPI/appsettings.json @@ -0,0 +1,16 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*", + + "ConnectionStrings": { + "DefaultConnection": "ConnectionString", + "Redis": "Redis" + }, + + +} diff --git a/Streetcode/Streetcode.Email.XUnitTest/Email/EmailCommandValidatorTests.cs b/Streetcode/Streetcode.Email.XUnitTest/Email/EmailCommandValidatorTests.cs new file mode 100644 index 0000000..b8d6c6c --- /dev/null +++ b/Streetcode/Streetcode.Email.XUnitTest/Email/EmailCommandValidatorTests.cs @@ -0,0 +1,70 @@ +namespace Streetcode.Email.XUnitTest.MediatR.Email +{ + using FluentAssertions; + using FluentValidation.TestHelper; + using Streetcode.Email.BLL.DTO; + using Streetcode.Email.BLL.MediatR.Email; + using Streetcode.Resources; + using Xunit; + + public class EmailCommandValidatorTests + { + private readonly SendEmailCommandValidator validator; + + public EmailCommandValidatorTests() + { + this.validator = new SendEmailCommandValidator(); + } + + [Fact] + public void ShouldReturnError_IfEmailIsNull() + { + // Arrange + var command = new SendEmailCommand(null!); + + // Act + var result = this.validator.TestValidate(command); + + // Assert + result.ShouldHaveValidationErrorFor(x => x.email) + .WithErrorMessage(Messages.Error_CommandDataRequired); + } + + [Fact] + public void ShouldHaveError_WhenEmailDTOIsInvalid() + { + // Arrange + var invalidDto = new EmailDTO + { + From = "invalid-email", + Content = "123" + }; + var command = new SendEmailCommand(invalidDto); + + // Act + var result = this.validator.TestValidate(command); + + // Assert + result.ShouldHaveValidationErrorFor(x => x.email.From); + result.ShouldHaveValidationErrorFor(x => x.email.Content); + } + + [Fact] + public void ShouldNotHaveErrors_WhenCommandIsValid() + { + // Arrange + var validDto = new EmailDTO + { + From = "test@gmail.com", + Content = "Valid message content" + }; + var command = new SendEmailCommand(validDto); + + // Act + var result = this.validator.TestValidate(command); + + // Assert + result.ShouldNotHaveAnyValidationErrors(); + } + } +} \ No newline at end of file diff --git a/Streetcode/Streetcode.Email.XUnitTest/Email/EmailDTOValidatorTests.cs b/Streetcode/Streetcode.Email.XUnitTest/Email/EmailDTOValidatorTests.cs new file mode 100644 index 0000000..dbd42f9 --- /dev/null +++ b/Streetcode/Streetcode.Email.XUnitTest/Email/EmailDTOValidatorTests.cs @@ -0,0 +1,95 @@ +namespace Streetcode.Email.XUnitTest.MediatR.Email +{ + using FluentValidation.TestHelper; + using Streetcode.Email.BLL.DTO; + using Streetcode.Email.BLL.MediatR.Email; + using Xunit; + + public class EmailDTOValidatorTests + { + private readonly EmailDTOValidator validator; + + public EmailDTOValidatorTests() + { + this.validator = new EmailDTOValidator(); + } + + [Fact] + public void ShouldHaveError_WhenEmailIsEmpty() + { + // Arrange + var model = new EmailDTO { From = string.Empty }; + + // Act + var result = this.validator.TestValidate(model); + + // Assert + result.ShouldHaveValidationErrorFor(x => x.From); + } + + [Fact] + public void ShouldHaveError_WhenEmailIsInvalid() + { + // Arrange + var model = new EmailDTO { From = "not-an-email" }; + + // Act + var result = this.validator.TestValidate(model); + + // Assert + result.ShouldHaveValidationErrorFor(x => x.From) + .WithErrorCode("EmailValidator"); + } + + [Fact] + public void ShouldHaveError_WhenMessageIsTooShort() + { + // Arrange + var model = new EmailDTO + { + From = "test@gmail.com", + Content = "123" + }; + + // Act + var result = this.validator.TestValidate(model); + + // Assert + result.ShouldHaveValidationErrorFor(x => x.Content); + } + + [Fact] + public void ShouldHaveError_WhenMessageExceedsMaxLength() + { + // Arrange + var model = new EmailDTO + { + From = "test@gmail.com", + Content = new string('a', 1001) + }; + + // Act + var result = this.validator.TestValidate(model); + + // Assert + result.ShouldHaveValidationErrorFor(x => x.Content); + } + + [Fact] + public void ShouldNotHaveAnyValidationErrors_WhenDTOIsValid() + { + // Arrange + var model = new EmailDTO + { + From = "test@gmail.com", + Content = "Hello World!" + }; + + // Act + var result = this.validator.TestValidate(model); + + // Assert + result.ShouldNotHaveAnyValidationErrors(); + } + } +} \ No newline at end of file diff --git a/Streetcode/Streetcode.Email.XUnitTest/Email/SendEmailHandlerTests.cs b/Streetcode/Streetcode.Email.XUnitTest/Email/SendEmailHandlerTests.cs new file mode 100644 index 0000000..8c18411 --- /dev/null +++ b/Streetcode/Streetcode.Email.XUnitTest/Email/SendEmailHandlerTests.cs @@ -0,0 +1,103 @@ +namespace Streetcode.Email.XUnitTest.MediatR.Email +{ + using AutoMapper; + using FluentAssertions; + using Hangfire; + using Hangfire.Common; + using Hangfire.States; + using Microsoft.EntityFrameworkCore; + using Microsoft.Extensions.Logging; + using Moq; + using Streetcode.Email.BLL.DTO; + using Streetcode.Email.BLL.Interfaces; + using Streetcode.Email.BLL.MediatR.Email; + using Streetcode.Email.DAL.Persistence; + using Streetcode.Email.BLL.Mapping; + using Streetcode.Resources; + public class SendEmailHandlerTests : IDisposable + { + private readonly EmailDbContext dbContext; + private readonly Mock> mockLogger; + private readonly Mock mockBackgroundJob; + private readonly IMapper mapper; + private readonly SendEmailHandler handler; + + public SendEmailHandlerTests() + { + var options = new DbContextOptionsBuilder() + .UseInMemoryDatabase(databaseName: Guid.NewGuid().ToString()) + .Options; + + this.dbContext = new EmailDbContext(options); + this.mockLogger = new Mock>(); + this.mockBackgroundJob = new Mock(); + + var configuration = new MapperConfiguration(cfg => + { + cfg.AddProfile(new EmailProfile()); + }); + this.mapper = new Mapper(configuration); + + this.handler = new SendEmailHandler( + this.dbContext, + this.mapper, + this.mockLogger.Object, + this.mockBackgroundJob.Object); + } + + [Fact] + public async Task Handle_ShouldReturnOk_WhenEmailIsSavedSuccessfully() + { + // Arrange + var EmailDto = new EmailDTO { From = "test@gmail.com", Content = "Valid message" }; + var command = new SendEmailCommand(EmailDto); + + // Act + var result = await this.handler.Handle(command, CancellationToken.None); + + // Assert + result.IsSuccess.Should().BeTrue(); + this.mockBackgroundJob.Verify(x => x.Create( + It.Is(j => j.Method.Name == nameof(IEmailService.SendEmailAsync)), + It.IsAny()), Times.Once); + } + + [Fact] + public async Task Handle_ShouldReturnFail_IfDatabaseSaveFails() + { + // Arrange + var options = new DbContextOptionsBuilder() + .UseInMemoryDatabase(databaseName: "FailDatabase") + .Options; + + var mockContext = new Mock(options) { CallBase = true }; + + mockContext + .Setup(c => c.SaveChangesAsync(It.IsAny())) + .ReturnsAsync(0); + + var failHandler = new SendEmailHandler( + mockContext.Object, + this.mapper, + this.mockLogger.Object, + this.mockBackgroundJob.Object); + + var command = new SendEmailCommand(new EmailDTO { From = "fail@test.com", Content = "fail message" }); + + // Act + var result = await failHandler.Handle(command, CancellationToken.None); + + // Assert + result.IsFailed.Should().BeTrue(); + result.Errors.Should().ContainSingle(e => e.Message == Messages.Error_FailedToCreateEntity); + + this.mockBackgroundJob.Verify(x => x.Create(It.IsAny(), It.IsAny()), Times.Never); + } + + public void Dispose() + { + this.dbContext.Database.EnsureDeleted(); + this.dbContext.Dispose(); + } + } +} \ No newline at end of file diff --git a/Streetcode/Streetcode.Email.XUnitTest/Streetcode.Email.XUnitTest.csproj b/Streetcode/Streetcode.Email.XUnitTest/Streetcode.Email.XUnitTest.csproj new file mode 100644 index 0000000..623a4a8 --- /dev/null +++ b/Streetcode/Streetcode.Email.XUnitTest/Streetcode.Email.XUnitTest.csproj @@ -0,0 +1,31 @@ + + + + net8.0 + enable + enable + + false + true + + + + + + + + + + + + + + + + + + + + + + diff --git a/Streetcode/Streetcode.Shared/Contracts/IEmailMessage.cs b/Streetcode/Streetcode.Shared/Contracts/IEmailMessage.cs new file mode 100644 index 0000000..a13df68 --- /dev/null +++ b/Streetcode/Streetcode.Shared/Contracts/IEmailMessage.cs @@ -0,0 +1,8 @@ +namespace Streetcode.Shared.Contracts +{ + public interface IEmailMessage + { + public string From { get; } + public string Content { get; } + } +} diff --git a/Streetcode/Streetcode.WebApi/Controllers/Email/EmailController.cs b/Streetcode/Streetcode.WebApi/Controllers/Email/EmailController.cs index e85773a..e6c1192 100644 --- a/Streetcode/Streetcode.WebApi/Controllers/Email/EmailController.cs +++ b/Streetcode/Streetcode.WebApi/Controllers/Email/EmailController.cs @@ -1,17 +1,32 @@ +using MassTransit; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Streetcode.BLL.DTO.Email; -using Streetcode.BLL.MediatR.Email; +using Streetcode.Shared.Contracts; namespace Streetcode.WebApi.Controllers.Email { public class EmailController : BaseApiController { + private readonly IPublishEndpoint _publishEndpoint; + + public EmailController(IPublishEndpoint publishEndpoint) + { + _publishEndpoint = publishEndpoint; + } + [HttpPost] [AllowAnonymous] public async Task Send([FromBody] EmailDTO email) { - return HandleResult(await Mediator.Send(new SendEmailCommand(email))); + await _publishEndpoint.Publish( + new + { + email.From, + email.Content + }); + + return Accepted(); } } } diff --git a/Streetcode/Streetcode.WebApi/Extensions/ServiceCollectionExtensions.cs b/Streetcode/Streetcode.WebApi/Extensions/ServiceCollectionExtensions.cs index d48a9ae..6e75186 100644 --- a/Streetcode/Streetcode.WebApi/Extensions/ServiceCollectionExtensions.cs +++ b/Streetcode/Streetcode.WebApi/Extensions/ServiceCollectionExtensions.cs @@ -10,7 +10,6 @@ using Microsoft.OpenApi.Models; using Streetcode.BLL.Interfaces.BlobStorage; using Streetcode.BLL.Interfaces.Cache; -using Streetcode.BLL.Interfaces.Email; using Streetcode.BLL.Interfaces.Instagram; using Streetcode.BLL.Interfaces.Logging; using Streetcode.BLL.Interfaces.Payment; @@ -18,7 +17,6 @@ using Streetcode.BLL.MediatR.PipelineBehavior; using Streetcode.BLL.Services.BlobStorageService; using Streetcode.BLL.Services.Cache; -using Streetcode.BLL.Services.Email; using Streetcode.BLL.Services.Instagram; using Streetcode.BLL.Services.Logging; using Streetcode.BLL.Services.Payment; @@ -51,7 +49,6 @@ public static void AddCustomServices(this IServiceCollection services, IConfigur services.AddScoped(); services.AddScoped(); - services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); diff --git a/Streetcode/Streetcode.sln b/Streetcode/Streetcode.sln index 17164d8..fa08c0a 100644 --- a/Streetcode/Streetcode.sln +++ b/Streetcode/Streetcode.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 17 -VisualStudioVersion = 17.14.36717.8 d17.14 +# Visual Studio Version 18 +VisualStudioVersion = 18.3.11512.155 MinimumVisualStudioVersion = 10.0.40219.1 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Streetcode.WebApi", "Streetcode.WebApi\Streetcode.WebApi.csproj", "{CAA32FB4-F481-4748-BFCE-33B0DBF433E8}" EndProject @@ -20,6 +20,8 @@ EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{95E863CE-C3B4-4DBC-8C15-957E63BA45B6}" ProjectSection(SolutionItems) = preProject .editorconfig = .editorconfig + .env = .env + docker-compose.yml = docker-compose.yml EndProjectSection EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Services", "Services", "{02EA681E-C7D8-13C7-8484-4AC65E1B71E8}" @@ -40,6 +42,16 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Streetcode.Auth.XUnitTest", EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Streetcode.Resources", "Streetcode.Resources\Streetcode.Resources.csproj", "{42EC9582-F9D7-9F18-42AB-C0D335E17C5C}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Streetcode.Email", "Streetcode.Email", "{722B38C8-AA92-4391-8320-9CCBFCD6BBEF}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Streetcode.Email.WebAPI", "Streetcode.Email.WebAPI\Streetcode.Email.WebAPI.csproj", "{F3953CBD-E8CE-40AA-A219-DED4518B6334}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Streetcode.Email.BLL", "Streetcode.Email.BLL\Streetcode.Email.BLL.csproj", "{CC733440-BA59-431D-B9CC-BB2B7EC0091B}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Streetcode.Email.DAL", "Streetcode.Email.DAL\Streetcode.Email.DAL.csproj", "{072870A2-8064-40E2-B5C5-BF90B33F36BC}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Streetcode.Email.XUnitTest", "Streetcode.Email.XUnitTest\Streetcode.Email.XUnitTest.csproj", "{360844AB-D2B0-49B1-A500-EAFC0F8A7659}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -96,6 +108,22 @@ Global {42EC9582-F9D7-9F18-42AB-C0D335E17C5C}.Debug|Any CPU.Build.0 = Debug|Any CPU {42EC9582-F9D7-9F18-42AB-C0D335E17C5C}.Release|Any CPU.ActiveCfg = Release|Any CPU {42EC9582-F9D7-9F18-42AB-C0D335E17C5C}.Release|Any CPU.Build.0 = Release|Any CPU + {F3953CBD-E8CE-40AA-A219-DED4518B6334}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F3953CBD-E8CE-40AA-A219-DED4518B6334}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F3953CBD-E8CE-40AA-A219-DED4518B6334}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F3953CBD-E8CE-40AA-A219-DED4518B6334}.Release|Any CPU.Build.0 = Release|Any CPU + {CC733440-BA59-431D-B9CC-BB2B7EC0091B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {CC733440-BA59-431D-B9CC-BB2B7EC0091B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {CC733440-BA59-431D-B9CC-BB2B7EC0091B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {CC733440-BA59-431D-B9CC-BB2B7EC0091B}.Release|Any CPU.Build.0 = Release|Any CPU + {072870A2-8064-40E2-B5C5-BF90B33F36BC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {072870A2-8064-40E2-B5C5-BF90B33F36BC}.Debug|Any CPU.Build.0 = Debug|Any CPU + {072870A2-8064-40E2-B5C5-BF90B33F36BC}.Release|Any CPU.ActiveCfg = Release|Any CPU + {072870A2-8064-40E2-B5C5-BF90B33F36BC}.Release|Any CPU.Build.0 = Release|Any CPU + {360844AB-D2B0-49B1-A500-EAFC0F8A7659}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {360844AB-D2B0-49B1-A500-EAFC0F8A7659}.Debug|Any CPU.Build.0 = Debug|Any CPU + {360844AB-D2B0-49B1-A500-EAFC0F8A7659}.Release|Any CPU.ActiveCfg = Release|Any CPU + {360844AB-D2B0-49B1-A500-EAFC0F8A7659}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -107,6 +135,11 @@ Global {D89E738A-A455-4D17-9C9D-581AE177E5AC} = {F69E76E4-84F5-4D70-A52B-E588CCEE716B} {00D85447-2B32-4B68-A4C2-1B467C42FD16} = {7F5CBCE1-AD14-4C06-8939-B595C1B25B47} {DF36FE52-8788-4490-8406-62132A7E6756} = {F69E76E4-84F5-4D70-A52B-E588CCEE716B} + {722B38C8-AA92-4391-8320-9CCBFCD6BBEF} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} + {F3953CBD-E8CE-40AA-A219-DED4518B6334} = {722B38C8-AA92-4391-8320-9CCBFCD6BBEF} + {CC733440-BA59-431D-B9CC-BB2B7EC0091B} = {722B38C8-AA92-4391-8320-9CCBFCD6BBEF} + {072870A2-8064-40E2-B5C5-BF90B33F36BC} = {722B38C8-AA92-4391-8320-9CCBFCD6BBEF} + {360844AB-D2B0-49B1-A500-EAFC0F8A7659} = {722B38C8-AA92-4391-8320-9CCBFCD6BBEF} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {7D3D1FEF-DB8F-4A51-AD6E-0EE327AB534A} diff --git a/Streetcode/docker-compose.yml b/Streetcode/docker-compose.yml new file mode 100644 index 0000000..40b9ff9 --- /dev/null +++ b/Streetcode/docker-compose.yml @@ -0,0 +1,34 @@ +version: '3.8' + +services: + rabbitmq: + image: rabbitmq:3-management + container_name: streetcode-rabbitmq + restart: always + ports: + - "5672:5672" + - "15672:15672" + environment: + - RABBITMQ_DEFAULT_USER=${RABBITMQ_DEFAULT_USER} + - RABBITMQ_DEFAULT_PASS=${RABBITMQ_DEFAULT_PASS} + + redis: + image: redis:latest + container_name: streetcode-redis + restart: always + ports: + - "6379:6379" + + sqlserver: + image: mcr.microsoft.com/mssql/server:2022-latest + container_name: streetcode-email-db + ports: + - "1433:1433" + environment: + - ACCEPT_EULA=Y + - MSSQL_SA_PASSWORD=${DB_PASSWORD} + healthcheck: + test: ["CMD", "/opt/mssql-tools/bin/sqlcmd", "-U", "sa", "-P", "${DB_PASSWORD}", "-Q", "SELECT 1"] + interval: 10s + timeout: 3s + retries: 10 \ No newline at end of file