diff --git a/MiniSpace.APIGateway/src/MiniSpace.APIGateway/ntrada.yml b/MiniSpace.APIGateway/src/MiniSpace.APIGateway/ntrada.yml index 0a5b2acb0..731257a35 100644 --- a/MiniSpace.APIGateway/src/MiniSpace.APIGateway/ntrada.yml +++ b/MiniSpace.APIGateway/src/MiniSpace.APIGateway/ntrada.yml @@ -593,13 +593,44 @@ modules: downstream: posts-service/posts/{postId} auth: true - services: posts-service: localUrl: localhost:5013 url: posts-service + + - + mediafiles: + path: /media-files + routes: + - upstream: / + method: POST + use: downstream + downstream: mediafiles-service/media-files + auth: true + + - upstream: /{mediaFileId} + method: GET + use: downstream + downstream: mediafiles-service/media-files/{mediaFileId} + + - upstream: /{mediaFileId}/original + method: GET + use: downstream + downstream: mediafiles-service/media-files/{mediaFileId}/original + + - upstream: /{mediaFileId} + method: DELETE + use: downstream + downstream: mediafiles-service/media-files/{mediaFileId} + auth: true + + services: + mediafiles-service: + localUrl: localhost:5014 + url: mediafiles-service + + organizations: path: /organizations diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Api/Program.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Api/Program.cs index 19256117f..f49a31a8b 100644 --- a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Api/Program.cs +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Api/Program.cs @@ -47,7 +47,7 @@ public static async Task Main(string[] args) .UseDispatcherEndpoints(endpoints => endpoints .Get("events/{eventId}") .Put("events/{eventId}") - .Post("events", + .Post("events", afterDispatch: (cmd, ctx) => ctx.Response.Created($"events/{cmd.EventId}")) .Delete("events/{eventId}") .Post("events/{eventId}/sign-up") diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Commands/AddEvent.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Commands/CreateEvent.cs similarity index 76% rename from MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Commands/AddEvent.cs rename to MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Commands/CreateEvent.cs index 48cd1f552..13fc9bc4b 100644 --- a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Commands/AddEvent.cs +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Commands/CreateEvent.cs @@ -1,10 +1,12 @@ using System; +using System.Collections; +using System.Collections.Generic; using Convey.CQRS.Commands; using MiniSpace.Services.Events.Core.Entities; namespace MiniSpace.Services.Events.Application.Commands { - public class AddEvent : ICommand + public class CreateEvent : ICommand { public Guid EventId { get; } public string Name { get; } @@ -19,18 +21,19 @@ public class AddEvent : ICommand public string ApartmentNumber { get; } public string City { get; } public string ZipCode { get; } + public IEnumerable MediaFiles { get; } public string Description { get; } public int Capacity { get; } public decimal Fee { get; } public string Category { get; } public string PublishDate { get; } - public AddEvent(Guid eventId, string name, Guid organizerId, Guid organizationId, Guid rootOrganizationId, - string startDate, string endDate, string buildingName, string street, string buildingNumber, - string apartmentNumber, string city, string zipCode, string description, int capacity, decimal fee, - string category, string publishDate) + public CreateEvent(Guid eventId, string name, Guid organizerId, Guid organizationId, Guid rootOrganizationId, + string startDate, string endDate, string buildingName, string street, string buildingNumber, + string apartmentNumber, string city, string zipCode, IEnumerable mediaFiles, string description, + int capacity, decimal fee, string category, string publishDate) { - EventId = eventId == Guid.Empty ? Guid.NewGuid() : eventId; + EventId = eventId; Name = name; OrganizerId = organizerId; OrganizationId = organizationId; @@ -43,6 +46,7 @@ public AddEvent(Guid eventId, string name, Guid organizerId, Guid organizationId ApartmentNumber = apartmentNumber; City = city; ZipCode = zipCode; + MediaFiles = mediaFiles; Description = description; Capacity = capacity; Fee = fee; diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Commands/Handlers/AddEventHandler.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Commands/Handlers/CreateEventHandler.cs similarity index 85% rename from MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Commands/Handlers/AddEventHandler.cs rename to MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Commands/Handlers/CreateEventHandler.cs index c60f39e33..1c7e67580 100644 --- a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Commands/Handlers/AddEventHandler.cs +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Commands/Handlers/CreateEventHandler.cs @@ -12,7 +12,7 @@ namespace MiniSpace.Services.Events.Application.Commands.Handlers { - public class AddEventHandler: ICommandHandler + public class CreateEventHandler: ICommandHandler { private readonly IEventRepository _eventRepository; private readonly IMessageBroker _messageBroker; @@ -21,7 +21,7 @@ public class AddEventHandler: ICommandHandler private readonly IEventValidator _eventValidator; private readonly IAppContext _appContext; - public AddEventHandler(IEventRepository eventRepository, IMessageBroker messageBroker, + public CreateEventHandler(IEventRepository eventRepository, IMessageBroker messageBroker, IOrganizationsServiceClient organizationsServiceClient, IDateTimeProvider dateTimeProvider, IEventValidator eventValidator, IAppContext appContext) { @@ -33,13 +33,18 @@ public AddEventHandler(IEventRepository eventRepository, IMessageBroker messageB _appContext = appContext; } - public async Task HandleAsync(AddEvent command, CancellationToken cancellationToken) + public async Task HandleAsync(CreateEvent command, CancellationToken cancellationToken) { var identity = _appContext.Identity; if (!identity.IsOrganizer) throw new AuthorizedUserIsNotAnOrganizerException(identity.Id); if(identity.Id != command.OrganizerId) throw new OrganizerCannotAddEventForAnotherOrganizerException(identity.Id, command.OrganizerId); + + if (command.EventId == Guid.Empty || await _eventRepository.ExistsAsync(command.EventId)) + { + throw new InvalidEventIdException(command.EventId); + } _eventValidator.ValidateName(command.Name); _eventValidator.ValidateDescription(command.Description); @@ -50,6 +55,7 @@ public async Task HandleAsync(AddEvent command, CancellationToken cancellationTo _eventValidator.ValidateDates(startDate, endDate, "event_start_date", "event_end_date"); var address = new Address(command.BuildingName, command.Street, command.BuildingNumber, command.ApartmentNumber, command.City, command.ZipCode); + _eventValidator.ValidateMediaFiles(command.MediaFiles.ToList()); _eventValidator.ValidateCapacity(command.Capacity); _eventValidator.ValidateFee(command.Fee); var category = _eventValidator.ParseCategory(command.Category); @@ -77,10 +83,10 @@ public async Task HandleAsync(AddEvent command, CancellationToken cancellationTo var organizer = new Organizer(command.OrganizerId, identity.Name, identity.Email, command.OrganizationId, organization.Name); var @event = Event.Create(command.EventId, command.Name, command.Description, startDate, endDate, - address, command.Capacity, command.Fee, category, state, publishDate, organizer, now); + address, command.MediaFiles, command.Capacity, command.Fee, category, state, publishDate, organizer, now); await _eventRepository.AddAsync(@event); - await _messageBroker.PublishAsync(new EventCreated(@event.Id, @event.Organizer.Id)); + await _messageBroker.PublishAsync(new EventCreated(@event.Id, @event.Organizer.Id, @event.MediaFiles)); } } } \ No newline at end of file diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Commands/Handlers/UpdateEventHandler.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Commands/Handlers/UpdateEventHandler.cs index de8ea2789..8b689f914 100644 --- a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Commands/Handlers/UpdateEventHandler.cs +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Commands/Handlers/UpdateEventHandler.cs @@ -1,4 +1,5 @@ -using System.Threading; +using System.Linq; +using System.Threading; using System.Threading.Tasks; using Convey.CQRS.Commands; using MiniSpace.Services.Events.Application.Events; @@ -54,6 +55,7 @@ public async Task HandleAsync(UpdateEvent command, CancellationToken cancellatio var address = @event.Location.Update(command.BuildingName, command.Street, command.BuildingNumber, command.ApartmentNumber, command.City, command.ZipCode); + _eventValidator.ValidateMediaFiles(command.MediaFiles.ToList()); var capacity = command.Capacity == 0 ? @event.Capacity : command.Capacity; _eventValidator.ValidateUpdatedCapacity(capacity, @event.Capacity); var fee = command.Fee == 0 ? @event.Fee : command.Fee; @@ -71,7 +73,8 @@ public async Task HandleAsync(UpdateEvent command, CancellationToken cancellatio @event.Update(name, description, startDate, endDate, address, capacity, fee, category, state, publishDate, now); await _eventRepository.UpdateAsync(@event); - await _messageBroker.PublishAsync(new EventUpdated(@event.Id, _dateTimeProvider.Now, identity.Id)); + await _messageBroker.PublishAsync(new EventUpdated(@event.Id, _dateTimeProvider.Now, + identity.Id, @event.MediaFiles)); } } } \ No newline at end of file diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Commands/UpdateEvent.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Commands/UpdateEvent.cs index 7cc856df7..c58f4b248 100644 --- a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Commands/UpdateEvent.cs +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Commands/UpdateEvent.cs @@ -1,4 +1,6 @@ using System; +using System.Collections; +using System.Collections.Generic; using Convey.CQRS.Commands; namespace MiniSpace.Services.Events.Application.Commands @@ -16,6 +18,7 @@ public class UpdateEvent : ICommand public string ApartmentNumber { get; } public string City { get; } public string ZipCode { get; } + public IEnumerable MediaFiles { get; } public string Description { get; } public int Capacity { get; } public decimal Fee { get; } @@ -24,7 +27,8 @@ public class UpdateEvent : ICommand public UpdateEvent(Guid eventId, string name, Guid organizerId, string startDate, string endDate, string buildingName, string street, string buildingNumber, string apartmentNumber, string city, - string zipCode, string description, int capacity, decimal fee, string category, string publishDate) + string zipCode, IEnumerable mediaFiles, string description, int capacity, decimal fee, + string category, string publishDate) { EventId = eventId; Name = name; @@ -37,6 +41,7 @@ public UpdateEvent(Guid eventId, string name, Guid organizerId, string startDate ApartmentNumber = apartmentNumber; City = city; ZipCode = zipCode; + MediaFiles = mediaFiles; Description = description; Capacity = capacity; Fee = fee; diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/DTO/EventDto.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/DTO/EventDto.cs index 22881081b..ddc0f852b 100644 --- a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/DTO/EventDto.cs +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/DTO/EventDto.cs @@ -15,7 +15,7 @@ public class EventDto public DateTime StartDate { get; set; } public DateTime EndDate { get; set; } public AddressDto Location { get; set; } - //public string Image { get; set; } + public IEnumerable MediaFiles { get; set; } public int InterestedStudents { get; set; } public int SignedUpStudents { get; set; } public int Capacity { get; set; } @@ -43,6 +43,7 @@ public EventDto(Event @event, Guid studentId) StartDate = @event.StartDate; EndDate = @event.EndDate; Location = new AddressDto(@event.Location); + MediaFiles = @event.MediaFiles; InterestedStudents = @event.InterestedStudents.Count(); SignedUpStudents = @event.SignedUpStudents.Count(); Capacity = @event.Capacity; diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Events/EventCreated.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Events/EventCreated.cs index 0df57828b..7888984c9 100644 --- a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Events/EventCreated.cs +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Events/EventCreated.cs @@ -1,11 +1,13 @@ using System; +using System.Collections.Generic; using Convey.CQRS.Events; namespace MiniSpace.Services.Events.Application.Events { - public class EventCreated(Guid eventId, Guid organizerId) : IEvent + public class EventCreated(Guid eventId, Guid organizerId, IEnumerable mediaFilesIds) : IEvent { public Guid EventId { get; set; } = eventId; public Guid OrganizerId { get; set; } = organizerId; + public IEnumerable MediaFilesIds { get; set; } = mediaFilesIds; } } \ No newline at end of file diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Events/EventDeleted.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Events/EventDeleted.cs index a39a19cb8..9c74a3d37 100644 --- a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Events/EventDeleted.cs +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Events/EventDeleted.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using Convey.CQRS.Events; namespace MiniSpace.Services.Events.Application.Events diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Events/EventUpdated.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Events/EventUpdated.cs index c0e79da44..ce9291901 100644 --- a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Events/EventUpdated.cs +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Events/EventUpdated.cs @@ -1,12 +1,15 @@ using System; +using System.Collections; +using System.Collections.Generic; using Convey.CQRS.Events; namespace MiniSpace.Services.Events.Application.Events { - public class EventUpdated(Guid eventId, DateTime updatedAt, Guid updatedBy) : IEvent + public class EventUpdated(Guid eventId, DateTime updatedAt, Guid updatedBy, IEnumerable mediaFilesIds) : IEvent { public Guid EventId { get; set; } = eventId; public DateTime UpdatedAt { get; set; } = updatedAt; public Guid UpdatedBy { get; set; } = updatedBy; + public IEnumerable MediaFilesIds { get; set; } = mediaFilesIds; } } \ No newline at end of file diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Events/External/Handlers/MediaFileDeletedHandler.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Events/External/Handlers/MediaFileDeletedHandler.cs new file mode 100644 index 000000000..74369f69c --- /dev/null +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Events/External/Handlers/MediaFileDeletedHandler.cs @@ -0,0 +1,32 @@ +using System.Threading; +using System.Threading.Tasks; +using Convey.CQRS.Events; +using MiniSpace.Services.Events.Core.Repositories; + +namespace MiniSpace.Services.Events.Application.Events.External.Handlers +{ + public class MediaFileDeletedHandler: IEventHandler + { + private readonly IEventRepository _eventRepository; + + public MediaFileDeletedHandler(IEventRepository eventRepository) + { + _eventRepository = eventRepository; + } + + public async Task HandleAsync(MediaFileDeleted @event, CancellationToken cancellationToken) + { + if(@event.Source.ToLowerInvariant() != "event") + { + return; + } + + var foundEvent = await _eventRepository.GetAsync(@event.SourceId); + if(foundEvent != null) + { + foundEvent.RemoveMediaFile(@event.MediaFileId); + await _eventRepository.UpdateAsync(foundEvent); + } + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Events/External/MediaFileDeleted.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Events/External/MediaFileDeleted.cs new file mode 100644 index 000000000..b3b300b9d --- /dev/null +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Events/External/MediaFileDeleted.cs @@ -0,0 +1,21 @@ +using System; +using Convey.CQRS.Events; +using Convey.MessageBrokers; + +namespace MiniSpace.Services.Events.Application.Events.External +{ + [Message("mediafiles")] + public class MediaFileDeleted: IEvent + { + public Guid MediaFileId { get; } + public Guid SourceId { get; } + public string Source { get; } + + public MediaFileDeleted(Guid mediaFileId, Guid sourceId, string source) + { + MediaFileId = mediaFileId; + SourceId = sourceId; + Source = source; + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Events/Rejected/AddEventRejected.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Events/Rejected/CreateEventRejected.cs similarity index 78% rename from MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Events/Rejected/AddEventRejected.cs rename to MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Events/Rejected/CreateEventRejected.cs index 54e972866..50daa4279 100644 --- a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Events/Rejected/AddEventRejected.cs +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Events/Rejected/CreateEventRejected.cs @@ -5,7 +5,7 @@ namespace MiniSpace.Services.Events.Application.Events.Rejected { - public class AddEventRejected(Guid organizerId, string reason, string code) : IRejectedEvent + public class CreateEventRejected(Guid organizerId, string reason, string code) : IRejectedEvent { public Guid OrganizerId { get; } = organizerId; public string Reason { get; } = reason; diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Exceptions/InvalidEventIdException.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Exceptions/InvalidEventIdException.cs new file mode 100644 index 000000000..89fcea7f5 --- /dev/null +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Exceptions/InvalidEventIdException.cs @@ -0,0 +1,15 @@ +using System; + +namespace MiniSpace.Services.Events.Application.Exceptions +{ + public class InvalidEventIdException : AppException + { + public override string Code { get; } = "invalid_event_id"; + public Guid EventId { get; } + + public InvalidEventIdException(Guid eventId) : base($"Invalid event id: {eventId}.") + { + EventId = eventId; + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Exceptions/InvalidNumberOfEventMediaFilesException.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Exceptions/InvalidNumberOfEventMediaFilesException.cs new file mode 100644 index 000000000..8c1fe1627 --- /dev/null +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Exceptions/InvalidNumberOfEventMediaFilesException.cs @@ -0,0 +1,15 @@ +using System; + +namespace MiniSpace.Services.Events.Application.Exceptions +{ + public class InvalidNumberOfEventMediaFilesException : AppException + { + public override string Code { get; } = "invalid_number_of_event_media_files"; + public int MediaFilesNumber { get; } + public InvalidNumberOfEventMediaFilesException(int mediaFilesNumber) + : base($"Invalid media files number: {mediaFilesNumber}. It must be less or equal 5.") + { + MediaFilesNumber = mediaFilesNumber; + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Services/IEventValidator.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Services/IEventValidator.cs index cf1ad68b6..2f15e6ec2 100644 --- a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Services/IEventValidator.cs +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Application/Services/IEventValidator.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using MiniSpace.Services.Events.Core.Entities; namespace MiniSpace.Services.Events.Application.Services @@ -13,6 +14,7 @@ public interface IEventValidator (int pageNumber, int pageSize) PageFilter(int pageNumber, int pageSize); void ValidateName(string name); void ValidateDescription(string description); + void ValidateMediaFiles(List mediaFiles); void ValidateCapacity(int capacity); void ValidateFee(decimal fee); void ValidateUpdatedCapacity(int currentCapacity, int newCapacity); diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Core/Entities/Event.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Core/Entities/Event.cs index 28f5788a4..39816b87b 100644 --- a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Core/Entities/Event.cs +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Core/Entities/Event.cs @@ -17,7 +17,7 @@ public class Event: AggregateRoot public DateTime StartDate { get; private set; } public DateTime EndDate { get; private set; } public Address Location { get; private set; } - //public string Image { get; set; } + public IEnumerable MediaFiles { get; set; } public int Capacity { get; private set; } public decimal Fee { get; private set; } public Category Category { get; private set; } @@ -44,8 +44,8 @@ public IEnumerable Ratings } public Event(AggregateId id, string name, string description, DateTime startDate, DateTime endDate, - Address location, int capacity, decimal fee, Category category, State state, DateTime publishDate, - Organizer organizer, DateTime updatedAt, IEnumerable interestedStudents = null, + Address location, IEnumerable mediaFiles, int capacity, decimal fee, Category category, State state, + DateTime publishDate, Organizer organizer, DateTime updatedAt, IEnumerable interestedStudents = null, IEnumerable signedUpStudents = null, IEnumerable ratings = null) { Id = id; @@ -54,6 +54,7 @@ public Event(AggregateId id, string name, string description, DateTime startDat StartDate = startDate; EndDate = endDate; Location = location; + MediaFiles = mediaFiles; Capacity = capacity; Fee = fee; Category = category; @@ -67,11 +68,11 @@ public Event(AggregateId id, string name, string description, DateTime startDat } public static Event Create(AggregateId id, string name, string description, DateTime startDate, DateTime endDate, - Address location, int capacity, decimal fee, Category category, State state, DateTime publishDate, - Organizer organizer, DateTime now) + Address location, IEnumerable mediaFiles, int capacity, decimal fee, Category category, State state, + DateTime publishDate, Organizer organizer, DateTime now) { - var @event = new Event(id, name, description, startDate, endDate, location, capacity, fee, category, - state, publishDate, organizer, now); + var @event = new Event(id, name, description, startDate, endDate, location, mediaFiles, capacity, fee, + category, state, publishDate, organizer, now); return @event; } @@ -219,6 +220,15 @@ private void ChangeState(State state) State = state; } + public void RemoveMediaFile(Guid mediaFileId) + { + var mediaFile = MediaFiles.SingleOrDefault(mf => mf == mediaFileId); + if (mediaFile == Guid.Empty) + { + throw new MediaFileNotFoundException(mediaFileId, Id); + } + } + public bool IsOrganizer(Guid organizerId) => Organizer.Id == organizerId; } diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Core/Exceptions/MediaFileNotFoundException.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Core/Exceptions/MediaFileNotFoundException.cs new file mode 100644 index 000000000..305ba98d0 --- /dev/null +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Core/Exceptions/MediaFileNotFoundException.cs @@ -0,0 +1,18 @@ +using System; + +namespace MiniSpace.Services.Events.Core.Exceptions +{ + public class MediaFileNotFoundException : DomainException + { + public override string Code { get; } = "media_file_not_found"; + public Guid MediaFileId { get; } + public Guid EventId { get; } + + public MediaFileNotFoundException(Guid mediaFileId, Guid eventId) + : base($"Media file with ID: '{mediaFileId}' was not found for event with ID: {eventId}.") + { + MediaFileId = mediaFileId; + EventId = eventId; + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Core/Repositories/IEventRepository.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Core/Repositories/IEventRepository.cs index 321ee0d90..d172c4481 100644 --- a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Core/Repositories/IEventRepository.cs +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Core/Repositories/IEventRepository.cs @@ -13,6 +13,7 @@ public interface IEventRepository Task AddAsync(Event @event); Task UpdateAsync(Event @event); Task DeleteAsync(Guid id); + Task ExistsAsync(Guid id); Task<(IEnumerable events, int pageNumber,int pageSize, int totalPages, int totalElements)> BrowseEventsAsync( int pageNumber, int pageSize, string name, string organizer, DateTime dateFrom, DateTime dateTo, Category? category, State? state, IEnumerable organizations, IEnumerable friends, diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Exceptions/ExceptionToMessageMapper.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Exceptions/ExceptionToMessageMapper.cs index 28d680eea..75f68674f 100644 --- a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Exceptions/ExceptionToMessageMapper.cs +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Exceptions/ExceptionToMessageMapper.cs @@ -12,7 +12,7 @@ public object Map(Exception exception, object message) => exception switch { // TODO: Add more exceptions - AuthorizedUserIsNotAnOrganizerException ex => new AddEventRejected(ex.UserId, ex.Message, ex.Code), + AuthorizedUserIsNotAnOrganizerException ex => new CreateEventRejected(ex.UserId, ex.Message, ex.Code), EventNotFoundException ex => message switch { @@ -30,25 +30,25 @@ EventNotFoundException ex InvalidEventCategoryException ex => message switch { - AddEvent m => new AddEventRejected(m.OrganizerId, ex.Message, ex.Code), + CreateEvent m => new CreateEventRejected(m.OrganizerId, ex.Message, ex.Code), _ => null }, InvalidEventDateTimeException ex => message switch { - AddEvent m => new AddEventRejected(m.OrganizerId, ex.Message, ex.Code), + CreateEvent m => new CreateEventRejected(m.OrganizerId, ex.Message, ex.Code), _ => null }, InvalidEventDateTimeOrderException ex => message switch { - AddEvent m => new AddEventRejected(m.OrganizerId, ex.Message, ex.Code), + CreateEvent m => new CreateEventRejected(m.OrganizerId, ex.Message, ex.Code), _ => null }, OrganizerCannotAddEventForAnotherOrganizerException ex => message switch { - AddEvent m => new AddEventRejected(m.OrganizerId, ex.Message, ex.Code), + CreateEvent m => new CreateEventRejected(m.OrganizerId, ex.Message, ex.Code), _ => null }, StudentNotFoundException ex diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Extensions.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Extensions.cs index 49a53898c..f38ac1665 100644 --- a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Extensions.cs +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Extensions.cs @@ -103,7 +103,7 @@ public static IApplicationBuilder UseInfrastructure(this IApplicationBuilder app .UseMetrics() .UseAuthentication() .UseRabbitMq() - .SubscribeCommand() + .SubscribeCommand() .SubscribeCommand() .SubscribeCommand() .SubscribeCommand() diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Logging/MessageToLogTemplateMapper.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Logging/MessageToLogTemplateMapper.cs index f9dec514b..614512d2b 100644 --- a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Logging/MessageToLogTemplateMapper.cs +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Logging/MessageToLogTemplateMapper.cs @@ -14,10 +14,10 @@ private static IReadOnlyDictionary MessageTemplates => new Dictionary { { - typeof(AddEvent), + typeof(CreateEvent), new HandlerLogTemplate { - After = "Added an event with id: {EventId}." + After = "Created an event with id: {EventId}." } }, { diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Mongo/Documents/EventDocument.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Mongo/Documents/EventDocument.cs index dffb5c2f7..6ac0ea993 100644 --- a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Mongo/Documents/EventDocument.cs +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Mongo/Documents/EventDocument.cs @@ -15,7 +15,7 @@ public class EventDocument : IIdentifiable public DateTime StartDate { get; set; } public DateTime EndDate { get; set; } public Address Location { get; set; } - //public string Image { get; set; } + public IEnumerable MediaFiles { get; set; } public IEnumerable InterestedStudents { get; set; } public IEnumerable SignedUpStudents { get; set; } public int Capacity { get; set; } diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Mongo/Documents/Extensions.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Mongo/Documents/Extensions.cs index 855a05bcc..120646377 100644 --- a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Mongo/Documents/Extensions.cs +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Mongo/Documents/Extensions.cs @@ -18,6 +18,7 @@ public static EventDto AsDto(this EventDocument document, Guid studentId) StartDate = document.StartDate, EndDate = document.EndDate, Location = document.Location.AsDto(), + MediaFiles = document.MediaFiles, InterestedStudents = document.InterestedStudents.Count(), SignedUpStudents = document.SignedUpStudents.Count(), Capacity = document.Capacity, @@ -45,8 +46,9 @@ public static EventDto AsDtoWithFriends(this EventDocument document, Guid studen public static Event AsEntity(this EventDocument document) => new (document.Id, document.Name, document.Description, document.StartDate, document.EndDate, - document.Location, document.Capacity, document.Fee, document.Category, document.State, document.PublishDate, - document.Organizer, document.UpdatedAt,document.InterestedStudents, document.SignedUpStudents, document.Ratings); + document.Location, document.MediaFiles, document.Capacity, document.Fee, document.Category, + document.State, document.PublishDate, document.Organizer, document.UpdatedAt,document.InterestedStudents, + document.SignedUpStudents, document.Ratings); public static EventDocument AsDocument(this Event entity) => new () @@ -58,6 +60,7 @@ public static EventDocument AsDocument(this Event entity) StartDate = entity.StartDate, EndDate = entity.EndDate, Location = entity.Location, + MediaFiles = entity.MediaFiles, InterestedStudents = entity.InterestedStudents, SignedUpStudents = entity.SignedUpStudents, Capacity = entity.Capacity, diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Mongo/Repositories/EventMongoRepository.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Mongo/Repositories/EventMongoRepository.cs index e9015376e..a6b084d3c 100644 --- a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Mongo/Repositories/EventMongoRepository.cs +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Mongo/Repositories/EventMongoRepository.cs @@ -102,5 +102,6 @@ public async Task> GetAllAsync() public Task AddAsync(Event @event) => _repository.AddAsync(@event.AsDocument()); public Task UpdateAsync(Event @event) => _repository.UpdateAsync(@event.AsDocument()); public Task DeleteAsync(Guid id) => _repository.DeleteAsync(id); + public Task ExistsAsync(Guid id) => _repository.ExistsAsync(e => e.Id == id); } } \ No newline at end of file diff --git a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Services/EventValidator.cs b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Services/EventValidator.cs index 780893f12..351272dec 100644 --- a/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Services/EventValidator.cs +++ b/MiniSpace.Services.Events/src/MiniSpace.Services.Events.Infrastructure/Services/EventValidator.cs @@ -1,5 +1,8 @@ using System; +using System.Collections; +using System.Collections.Generic; using System.Globalization; +using System.Linq; using MiniSpace.Services.Events.Application.Exceptions; using MiniSpace.Services.Events.Application.Services; using MiniSpace.Services.Events.Core.Entities; @@ -73,6 +76,12 @@ public void ValidateDescription(string description) throw new InvalidEventDescriptionException(description); } + public void ValidateMediaFiles(List mediaFiles) + { + if (mediaFiles.Count > 5) + throw new InvalidNumberOfEventMediaFilesException(mediaFiles.Count); + } + public void ValidateCapacity(int capacity) { if (capacity <= 0 || capacity > 1000) diff --git a/MiniSpace.Services.MediaFiles/Dockerfile b/MiniSpace.Services.MediaFiles/Dockerfile new file mode 100644 index 000000000..17d4594fb --- /dev/null +++ b/MiniSpace.Services.MediaFiles/Dockerfile @@ -0,0 +1,17 @@ +FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build +WORKDIR /app + +COPY . . + +RUN dotnet publish src/MiniSpace.Services.MediaFiles.Api -c Release -o out + +FROM mcr.microsoft.com/dotnet/aspnet:8.0 +WORKDIR /app + +COPY --from=build /app/out . + +ENV ASPNETCORE_URLS=http://*:80 +ENV ASPNETCORE_ENVIRONMENT=docker +ENV NTRADA_CONFIG=ntrada.docker + +ENTRYPOINT ["dotnet", "MiniSpace.Services.MediaFiles.Api.dll"] diff --git a/MiniSpace.Services.MediaFiles/LICENSE b/MiniSpace.Services.MediaFiles/LICENSE new file mode 100644 index 000000000..b7ea7f0cc --- /dev/null +++ b/MiniSpace.Services.MediaFiles/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2019 DevMentors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/MiniSpace.Services.MediaFiles/MiniSpace.Services.MediaFiles.sln b/MiniSpace.Services.MediaFiles/MiniSpace.Services.MediaFiles.sln new file mode 100644 index 000000000..c008a6e7e --- /dev/null +++ b/MiniSpace.Services.MediaFiles/MiniSpace.Services.MediaFiles.sln @@ -0,0 +1,48 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.0.31903.59 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{19970311-BECA-46BA-9763-C8CE5FBEC34C}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MiniSpace.Services.MediaFiles.Api", "src\MiniSpace.Services.MediaFiles.Api\MiniSpace.Services.MediaFiles.Api.csproj", "{E3332633-A8EA-47DD-95BE-2E4AF82A25A1}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MiniSpace.Services.MediaFiles.Application", "src\MiniSpace.Services.MediaFiles.Application\MiniSpace.Services.MediaFiles.Application.csproj", "{85A84271-21EA-4E82-8023-115BA562BF22}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MiniSpace.Services.MediaFiles.Core", "src\MiniSpace.Services.MediaFiles.Core\MiniSpace.Services.MediaFiles.Core.csproj", "{DEED64BD-467A-4880-B078-C4E9FF257CB3}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MiniSpace.Services.MediaFiles.Infrastructure", "src\MiniSpace.Services.MediaFiles.Infrastructure\MiniSpace.Services.MediaFiles.Infrastructure.csproj", "{D8F4492A-C273-48FE-9611-1AC8E016944B}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {E3332633-A8EA-47DD-95BE-2E4AF82A25A1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E3332633-A8EA-47DD-95BE-2E4AF82A25A1}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E3332633-A8EA-47DD-95BE-2E4AF82A25A1}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E3332633-A8EA-47DD-95BE-2E4AF82A25A1}.Release|Any CPU.Build.0 = Release|Any CPU + {85A84271-21EA-4E82-8023-115BA562BF22}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {85A84271-21EA-4E82-8023-115BA562BF22}.Debug|Any CPU.Build.0 = Debug|Any CPU + {85A84271-21EA-4E82-8023-115BA562BF22}.Release|Any CPU.ActiveCfg = Release|Any CPU + {85A84271-21EA-4E82-8023-115BA562BF22}.Release|Any CPU.Build.0 = Release|Any CPU + {DEED64BD-467A-4880-B078-C4E9FF257CB3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {DEED64BD-467A-4880-B078-C4E9FF257CB3}.Debug|Any CPU.Build.0 = Debug|Any CPU + {DEED64BD-467A-4880-B078-C4E9FF257CB3}.Release|Any CPU.ActiveCfg = Release|Any CPU + {DEED64BD-467A-4880-B078-C4E9FF257CB3}.Release|Any CPU.Build.0 = Release|Any CPU + {D8F4492A-C273-48FE-9611-1AC8E016944B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D8F4492A-C273-48FE-9611-1AC8E016944B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D8F4492A-C273-48FE-9611-1AC8E016944B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D8F4492A-C273-48FE-9611-1AC8E016944B}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {E3332633-A8EA-47DD-95BE-2E4AF82A25A1} = {19970311-BECA-46BA-9763-C8CE5FBEC34C} + {85A84271-21EA-4E82-8023-115BA562BF22} = {19970311-BECA-46BA-9763-C8CE5FBEC34C} + {DEED64BD-467A-4880-B078-C4E9FF257CB3} = {19970311-BECA-46BA-9763-C8CE5FBEC34C} + {D8F4492A-C273-48FE-9611-1AC8E016944B} = {19970311-BECA-46BA-9763-C8CE5FBEC34C} + EndGlobalSection +EndGlobal diff --git a/MiniSpace.Services.MediaFiles/scripts/build.sh b/MiniSpace.Services.MediaFiles/scripts/build.sh new file mode 100644 index 000000000..3affad0eb --- /dev/null +++ b/MiniSpace.Services.MediaFiles/scripts/build.sh @@ -0,0 +1,2 @@ +#!/bin/bash +dotnet build -c release \ No newline at end of file diff --git a/MiniSpace.Services.MediaFiles/scripts/dockerize-tag-push.sh b/MiniSpace.Services.MediaFiles/scripts/dockerize-tag-push.sh new file mode 100644 index 000000000..456d4e3c7 --- /dev/null +++ b/MiniSpace.Services.MediaFiles/scripts/dockerize-tag-push.sh @@ -0,0 +1,11 @@ +#!/bin/bash + +export ASPNETCORE_ENVIRONMENT=docker + +cd .. + +docker build -t minispace.services.mediafiles:latest . + +docker tag minispace.services.mediafiles:latest adrianvsaint/minispace.services.mediafiles:latest + +docker push adrianvsaint/minispace.services.mediafiles:latest diff --git a/MiniSpace.Services.MediaFiles/scripts/dockerize.sh b/MiniSpace.Services.MediaFiles/scripts/dockerize.sh new file mode 100644 index 000000000..cfae96799 --- /dev/null +++ b/MiniSpace.Services.MediaFiles/scripts/dockerize.sh @@ -0,0 +1,21 @@ +#!/bin/bash +TAG='' +VERSION_TAG= + +case "$TRAVIS_BRANCH" in + "master") + TAG=latest + VERSION_TAG=$TRAVIS_BUILD_NUMBER + ;; + "develop") + TAG=dev + VERSION_TAG=$TAG-$TRAVIS_BUILD_NUMBER + ;; +esac + +REPOSITORY=$DOCKER_USERNAME/minispace.services.mediafiles + +docker login -u $DOCKER_USERNAME -p $DOCKER_PASSWORD +docker build -t $REPOSITORY:$TAG -t $REPOSITORY:$VERSION_TAG . +docker push $REPOSITORY:$TAG +docker push $REPOSITORY:$VERSION_TAG \ No newline at end of file diff --git a/MiniSpace.Services.MediaFiles/scripts/start.sh b/MiniSpace.Services.MediaFiles/scripts/start.sh new file mode 100644 index 000000000..ba33d0a20 --- /dev/null +++ b/MiniSpace.Services.MediaFiles/scripts/start.sh @@ -0,0 +1,4 @@ +#!/bin/bash +export ASPNETCORE_ENVIRONMENT=local +cd src/MiniSpace.Services.MediaFiles.Api +dotnet run diff --git a/MiniSpace.Services.MediaFiles/scripts/test.sh b/MiniSpace.Services.MediaFiles/scripts/test.sh new file mode 100644 index 000000000..6046c35a0 --- /dev/null +++ b/MiniSpace.Services.MediaFiles/scripts/test.sh @@ -0,0 +1,2 @@ +#!/bin/bash +dotnet test \ No newline at end of file diff --git a/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Api/MiniSpace.Services.MediaFiles.Api.csproj b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Api/MiniSpace.Services.MediaFiles.Api.csproj new file mode 100644 index 000000000..667a9e009 --- /dev/null +++ b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Api/MiniSpace.Services.MediaFiles.Api.csproj @@ -0,0 +1,21 @@ + + + + net8.0 + disable + enable + true + + + + + + + + + + + + + + diff --git a/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Api/Program.cs b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Api/Program.cs new file mode 100644 index 000000000..ea28dcc14 --- /dev/null +++ b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Api/Program.cs @@ -0,0 +1,51 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using Convey; +using Convey.Logging; +using Convey.Types; +using Convey.WebApi; +using Convey.WebApi.CQRS; +using Microsoft.AspNetCore; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.DependencyInjection; +using MiniSpace.Services.MediaFiles.Application; +using MiniSpace.Services.MediaFiles.Application.Commands; +using MiniSpace.Services.MediaFiles.Application.Dto; +using MiniSpace.Services.MediaFiles.Application.Queries; +using MiniSpace.Services.MediaFiles.Application.Services; +using MiniSpace.Services.MediaFiles.Infrastructure; + +namespace MiniSpace.Services.MediaFiles.Api +{ + public class Program + { + public static async Task Main(string[] args) + => await WebHost.CreateDefaultBuilder(args) + .ConfigureServices(services => services + .AddConvey() + .AddWebApi() + .AddApplication() + .AddInfrastructure() + .Build()) + .Configure(app => app + .UseInfrastructure() + .UseEndpoints(endpoints => endpoints + .Post("media-files", async (cmd, ctx) => + { + var fileId = await ctx.RequestServices.GetService().UploadAsync(cmd); + await ctx.Response.WriteJsonAsync(fileId); + }) + ) + .UseDispatcherEndpoints(endpoints => endpoints + .Get("", ctx => ctx.Response.WriteAsync(ctx.RequestServices.GetService().Name)) + .Get("media-files/{mediaFileId}") + .Get("media-files/{mediaFileId}/original") + .Delete("media-files/{mediaFileId}") + )) + .UseLogging() + .Build() + .RunAsync(); + } +} diff --git a/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Api/Properties/launchSettings.json b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Api/Properties/launchSettings.json new file mode 100644 index 000000000..3387cd0a6 --- /dev/null +++ b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Api/Properties/launchSettings.json @@ -0,0 +1,26 @@ +{ + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:5014" + } + }, + "profiles": { + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": false, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "local" + } + }, + "MiniSpace.Services.Events": { + "commandName": "Project", + "launchBrowser": false, + "applicationUrl": "http://localhost:5014", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "local" + } + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Api/appsettings.Development.json b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Api/appsettings.Development.json new file mode 100644 index 000000000..7a73a41bf --- /dev/null +++ b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Api/appsettings.Development.json @@ -0,0 +1,2 @@ +{ +} \ No newline at end of file diff --git a/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Api/appsettings.docker.json b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Api/appsettings.docker.json new file mode 100644 index 000000000..5d8e69926 --- /dev/null +++ b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Api/appsettings.docker.json @@ -0,0 +1,153 @@ +{ + "app": { + "name": "MiniSpace Media Files Service", + "version": "1" + }, + "consul": { + "enabled": true, + "url": "http://consul:8500", + "service": "mediafiles-service", + "address": "mediafiles-service", + "port": "80", + "pingEnabled": true, + "pingEndpoint": "ping", + "pingInterval": 3, + "removeAfterInterval": 3 + }, + "fabio": { + "enabled": true, + "url": "http://fabio:9999", + "service": "mediafiles-service" + }, + "httpClient": { + "type": "fabio", + "retries": 3, + "services": {} + }, + "jwt": { + "certificate": { + "location": "", + "password": "", + "rawData": "" + }, + "issuerSigningKey": "eiquief5phee9pazo0Faegaez9gohThailiur5woy2befiech1oarai4aiLi6ahVecah3ie9Aiz6Peij", + "expiryMinutes": 60, + "issuer": "minispace", + "validateAudience": false, + "validateIssuer": false, + "validateLifetime": true, + "allowAnonymousEndpoints": ["/sign-in", "/sign-up"] + }, + "logger": { + "console": { + "enabled": true + }, + "elk": { + "enabled": false, + "url": "http://elk:9200" + }, + "file": { + "enabled": false, + "path": "logs/logs.txt", + "interval": "day" + }, + "seq": { + "enabled": true, + "url": "http://seq:5341", + "apiKey": "secret" + } + }, + "jaeger": { + "enabled": true, + "serviceName": "events", + "udpHost": "jaeger", + "udpPort": 6831, + "maxPacketSize": 0, + "sampler": "const", + "excludePaths": ["/", "/ping", "/metrics"] + }, + "metrics": { + "enabled": true, + "influxEnabled": false, + "prometheusEnabled": true, + "influxUrl": "http://influx:8086", + "database": "minispace", + "env": "docker", + "interval": 5 + }, + "mongo": { + "connectionString": "mongodb+srv://minispace-user:9vd6IxYWUuuqhzEH@cluster0.mmhq4pe.mongodb.net/?retryWrites=true&w=majority&appName=Cluster0", + "database": "mediafiles-service", + "seed": false + }, + "rabbitMq": { + "connectionName": "mediafiles-service", + "retries": 3, + "retryInterval": 2, + "conventionsCasing": "snakeCase", + "logger": { + "enabled": true + }, + "username": "guest", + "password": "guest", + "virtualHost": "/", + "port": 5672, + "hostnames": [ + "rabbitmq" + ], + "requestedConnectionTimeout": "00:00:30", + "requestedHeartbeat": "00:01:00", + "socketReadTimeout": "00:00:30", + "socketWriteTimeout": "00:00:30", + "continuationTimeout": "00:00:20", + "handshakeContinuationTimeout": "00:00:10", + "networkRecoveryInterval": "00:00:05", + "exchange": { + "declare": true, + "durable": true, + "autoDelete": false, + "type": "topic", + "name": "events" + }, + "queue": { + "declare": true, + "durable": true, + "exclusive": false, + "autoDelete": false, + "template": "mediafiles-service/{{exchange}}.{{message}}" + }, + "context": { + "enabled": true, + "header": "message_context" + }, + "spanContextHeader": "span_context" + }, + "redis": { + "connectionString": "redis", + "instance": "mediafiles:" + }, + "swagger": { + "enabled": true, + "reDocEnabled": false, + "name": "v1", + "title": "API", + "version": "v1", + "routePrefix": "docs", + "includeSecurity": true + }, + "vault": { + "enabled": false, + "url": "http://vault:8200", + "kv": { + "enabled": false + }, + "pki": { + "enabled": false + }, + "lease": { + "mongo": { + "enabled": false + } + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Api/appsettings.json b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Api/appsettings.json new file mode 100644 index 000000000..10f68b8c8 --- /dev/null +++ b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Api/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Api/appsettings.local.json b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Api/appsettings.local.json new file mode 100644 index 000000000..5647c8fd6 --- /dev/null +++ b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Api/appsettings.local.json @@ -0,0 +1,195 @@ +{ + "app": { + "name": "MiniSpace Media Files Service", + "service": "mediafiles-service", + "version": "1" + }, + "consul": { + "enabled": true, + "url": "http://localhost:8500", + "service": "mediafiles-service", + "address": "docker.for.win.localhost", + "port": "5014", + "pingEnabled": false, + "pingEndpoint": "ping", + "pingInterval": 3, + "removeAfterInterval": 3 + }, + "fabio": { + "enabled": true, + "url": "http://localhost:9999", + "service": "mediafiles-service" + }, + "httpClient": { + "type": "direct", + "retries": 3, + "services": { + }, + "requestMasking": { + "enabled": true, + "maskTemplate": "*****" + } + }, + "jwt": { + "issuerSigningKey": "eiquief5phee9pazo0Faegaez9gohThailiur5woy2befiech1oarai4aiLi6ahVecah3ie9Aiz6Peij", + "expiryMinutes": 60, + "issuer": "minispace", + "validateAudience": false, + "validateIssuer": false, + "validateLifetime": false, + "allowAnonymousEndpoints": ["/sign-in", "/sign-up"] + }, + "logger": { + "level": "information", + "excludePaths": ["/", "/ping", "/metrics"], + "excludeProperties": [ + "api_key", + "access_key", + "ApiKey", + "ApiSecret", + "ClientId", + "ClientSecret", + "ConnectionString", + "Password", + "Email", + "Login", + "Secret", + "Token" + ], + "console": { + "enabled": true + }, + "elk": { + "enabled": false, + "url": "http://localhost:9200" + }, + "file": { + "enabled": true, + "path": "logs/logs.txt", + "interval": "day" + }, + "seq": { + "enabled": true, + "url": "http://localhost:5341", + "apiKey": "secret" + }, + "tags": {} + }, + "jaeger": { + "enabled": true, + "serviceName": "mediafiles", + "udpHost": "localhost", + "udpPort": 6831, + "maxPacketSize": 0, + "sampler": "const", + "excludePaths": ["/", "/ping", "/metrics"] + }, + "metrics": { + "enabled": true, + "influxEnabled": false, + "prometheusEnabled": true, + "influxUrl": "http://localhost:8086", + "database": "minispace", + "env": "local", + "interval": 5 + }, + "mongo": { + "connectionString": "mongodb+srv://minispace-user:9vd6IxYWUuuqhzEH@cluster0.mmhq4pe.mongodb.net/?retryWrites=true&w=majority&appName=Cluster0", + "database": "mediafiles-service", + "seed": false + }, + "outbox": { + "enabled": false, + "type": "sequential", + "expiry": 3600, + "intervalMilliseconds": 2000, + "inboxCollection": "inbox", + "outboxCollection": "outbox", + "disableTransactions": true + }, + "rabbitMq": { + "connectionName": "mediafiles-service", + "retries": 3, + "retryInterval": 2, + "conventionsCasing": "snakeCase", + "logger": { + "enabled": true + }, + "username": "guest", + "password": "guest", + "virtualHost": "/", + "port": 5672, + "hostnames": [ + "localhost" + ], + "requestedConnectionTimeout": "00:00:30", + "requestedHeartbeat": "00:01:00", + "socketReadTimeout": "00:00:30", + "socketWriteTimeout": "00:00:30", + "continuationTimeout": "00:00:20", + "handshakeContinuationTimeout": "00:00:10", + "networkRecoveryInterval": "00:00:05", + "exchange": { + "declare": true, + "durable": true, + "autoDelete": false, + "type": "topic", + "name": "mediafiles" + }, + "queue": { + "declare": true, + "durable": true, + "exclusive": false, + "autoDelete": false, + "template": "mediafiles-service/{{exchange}}.{{message}}" + }, + "context": { + "enabled": true, + "header": "message_context" + }, + "spanContextHeader": "span_context" + }, + "redis": { + "connectionString": "localhost", + "instance": "mediafiles:" + }, + "swagger": { + "enabled": true, + "reDocEnabled": false, + "name": "v1", + "title": "API", + "version": "v1", + "routePrefix": "docs", + "includeSecurity": true + }, + "vault": { + "enabled": true, + "url": "http://localhost:8200", + "authType": "token", + "token": "secret", + "username": "user", + "password": "secret", + "kv": { + "enabled": true, + "engineVersion": 2, + "mountPoint": "kv", + "path": "mediafiles-service/settings" + }, + "pki": { + "enabled": true, + "roleName": "mediafiles-service", + "commonName": "mediafiles-service.minispace.io" + }, + "lease": { + "mongo": { + "type": "database", + "roleName": "mediafiles-service", + "enabled": true, + "autoRenewal": true, + "templates": { + "connectionString": "mongodb://{{username}}:{{password}}@localhost:27017" + } + } + } + } +} diff --git a/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Commands/CleanupUnassociatedFiles.cs b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Commands/CleanupUnassociatedFiles.cs new file mode 100644 index 000000000..e97742e97 --- /dev/null +++ b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Commands/CleanupUnassociatedFiles.cs @@ -0,0 +1,14 @@ +using Convey.CQRS.Commands; + +namespace MiniSpace.Services.MediaFiles.Application.Commands +{ + public class CleanupUnassociatedFiles: ICommand + { + public DateTime Now { get; set; } + + public CleanupUnassociatedFiles(DateTime now) + { + Now = now; + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Commands/DeleteMediaFile.cs b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Commands/DeleteMediaFile.cs new file mode 100644 index 000000000..902fdefea --- /dev/null +++ b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Commands/DeleteMediaFile.cs @@ -0,0 +1,9 @@ +using Convey.CQRS.Commands; + +namespace MiniSpace.Services.MediaFiles.Application.Commands +{ + public class DeleteMediaFile: ICommand + { + public Guid MediaFileId { get; set; } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Commands/Handlers/CleanupUnassociatedFilesHandler.cs b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Commands/Handlers/CleanupUnassociatedFilesHandler.cs new file mode 100644 index 000000000..53946e804 --- /dev/null +++ b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Commands/Handlers/CleanupUnassociatedFilesHandler.cs @@ -0,0 +1,41 @@ +using Convey.CQRS.Commands; +using MiniSpace.Services.MediaFiles.Application.Events; +using MiniSpace.Services.MediaFiles.Application.Services; +using MiniSpace.Services.MediaFiles.Core.Entities; +using MiniSpace.Services.MediaFiles.Core.Repositories; + +namespace MiniSpace.Services.MediaFiles.Application.Commands.Handlers +{ + public class CleanupUnassociatedFilesHandler: ICommandHandler + { + private readonly IFileSourceInfoRepository _fileSourceInfoRepository; + private readonly IGridFSService _gridFSService; + private readonly IMessageBroker _messageBroker; + + public CleanupUnassociatedFilesHandler(IFileSourceInfoRepository fileSourceInfoRepository, IGridFSService gridFSService, + IMessageBroker messageBroker) + { + _fileSourceInfoRepository = fileSourceInfoRepository; + _gridFSService = gridFSService; + _messageBroker = messageBroker; + } + + public async Task HandleAsync(CleanupUnassociatedFiles command, CancellationToken cancellationToken) + { + var unassociatedFileSourceInfos = await _fileSourceInfoRepository.GetAllUnassociatedAsync(); + foreach (var file in unassociatedFileSourceInfos) + { + if ((command.Now - file.CreatedAt).TotalDays < 1) + { + continue; + } + + await _gridFSService.DeleteFileAsync(file.OriginalFileId); + await _gridFSService.DeleteFileAsync(file.FileId); + await _fileSourceInfoRepository.DeleteAsync(file.Id); + } + + await _messageBroker.PublishAsync(new UnassociatedFilesCleaned(command.Now)); + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Commands/Handlers/DeleteMediaFileHandler.cs b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Commands/Handlers/DeleteMediaFileHandler.cs new file mode 100644 index 000000000..d3c005d49 --- /dev/null +++ b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Commands/Handlers/DeleteMediaFileHandler.cs @@ -0,0 +1,46 @@ +using Convey.CQRS.Commands; +using MiniSpace.Services.MediaFiles.Application.Events; +using MiniSpace.Services.MediaFiles.Application.Exceptions; +using MiniSpace.Services.MediaFiles.Application.Services; +using MiniSpace.Services.MediaFiles.Core.Repositories; + +namespace MiniSpace.Services.MediaFiles.Application.Commands.Handlers +{ + public class DeleteMediaFileHandler: ICommandHandler + { + private readonly IFileSourceInfoRepository _fileSourceInfoRepository; + private readonly IGridFSService _gridFSService; + private readonly IAppContext _appContext; + private readonly IMessageBroker _messageBroker; + + public DeleteMediaFileHandler(IFileSourceInfoRepository fileSourceInfoRepository, IGridFSService gridFSService, + IAppContext appContext, IMessageBroker messageBroker) + { + _fileSourceInfoRepository = fileSourceInfoRepository; + _gridFSService = gridFSService; + _appContext = appContext; + _messageBroker = messageBroker; + } + + public async Task HandleAsync(DeleteMediaFile command, CancellationToken cancellationToken) + { + var fileSourceInfo = await _fileSourceInfoRepository.GetAsync(command.MediaFileId); + if (fileSourceInfo is null) + { + throw new MediaFileNotFoundException(command.MediaFileId); + } + + var identity = _appContext.Identity; + if(identity.IsAuthenticated && identity.Id != fileSourceInfo.UploaderId && !identity.IsAdmin) + { + throw new UnauthorizedMediaFileAccessException(fileSourceInfo.Id, identity.Id, fileSourceInfo.UploaderId); + } + + await _gridFSService.DeleteFileAsync(fileSourceInfo.OriginalFileId); + await _gridFSService.DeleteFileAsync(fileSourceInfo.FileId); + await _fileSourceInfoRepository.DeleteAsync(command.MediaFileId); + await _messageBroker.PublishAsync(new MediaFileDeleted(command.MediaFileId, + fileSourceInfo.SourceId, fileSourceInfo.SourceType.ToString())); + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Commands/UploadMediaFile.cs b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Commands/UploadMediaFile.cs new file mode 100644 index 000000000..459373162 --- /dev/null +++ b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Commands/UploadMediaFile.cs @@ -0,0 +1,28 @@ +using Convey.CQRS.Commands; +using Microsoft.AspNetCore.Http; + +namespace MiniSpace.Services.MediaFiles.Application.Commands +{ + public class UploadMediaFile : ICommand + { + public Guid MediaFileId { get; set; } + public Guid SourceId { get; set; } + public string SourceType { get; set; } + public Guid UploaderId { get; set; } + public string FileName { get; set; } + public string FileContentType { get; set; } + public string Base64Content { get; set; } + + public UploadMediaFile(Guid mediaFileId, Guid sourceId, string sourceType, Guid uploaderId, + string fileName, string fileContentType, string base64Content) + { + MediaFileId = mediaFileId == Guid.Empty ? Guid.NewGuid() : mediaFileId; + SourceId = sourceId; + SourceType = sourceType; + UploaderId = uploaderId; + FileName = fileName; + FileContentType = fileContentType; + Base64Content = base64Content; + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/ContractAttribute.cs b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/ContractAttribute.cs new file mode 100644 index 000000000..88c1e41a4 --- /dev/null +++ b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/ContractAttribute.cs @@ -0,0 +1,7 @@ +namespace MiniSpace.Services.MediaFiles.Application +{ + public class ContractAttribute : Attribute + { + + } +} diff --git a/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Dto/FileDto.cs b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Dto/FileDto.cs new file mode 100644 index 000000000..e794c5c31 --- /dev/null +++ b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Dto/FileDto.cs @@ -0,0 +1,29 @@ +namespace MiniSpace.Services.MediaFiles.Application.Dto +{ + public class FileDto + { + public Guid MediaFileId { get; set; } + public Guid SourceId { get; set; } + public string SourceType { get; set; } + public string State { get; set; } + public DateTime CreatedAt { get; set; } + public Guid UploaderId { get; set; } + public string FileName { get; set; } + public string FileContentType { get; set; } + public string Base64Content { get; set; } + + public FileDto(Guid mediaFileId, Guid sourceId, string sourceType, Guid uploaderId, string state, + DateTime createdAt, string fileName, string fileContentType, string base64Content) + { + MediaFileId = mediaFileId; + SourceId = sourceId; + SourceType = sourceType; + UploaderId = uploaderId; + State = state; + CreatedAt = createdAt; + FileName = fileName; + FileContentType = fileContentType; + Base64Content = base64Content; + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Dto/FileUploadResponseDto.cs b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Dto/FileUploadResponseDto.cs new file mode 100644 index 000000000..257152cb8 --- /dev/null +++ b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Dto/FileUploadResponseDto.cs @@ -0,0 +1,12 @@ +namespace MiniSpace.Services.MediaFiles.Application.Dto +{ + public class FileUploadResponseDto + { + public Guid FileId { get; set; } + + public FileUploadResponseDto(Guid fileId) + { + FileId = fileId; + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Events/External/EventCreated.cs b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Events/External/EventCreated.cs new file mode 100644 index 000000000..6bac7fdc7 --- /dev/null +++ b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Events/External/EventCreated.cs @@ -0,0 +1,18 @@ +using Convey.CQRS.Events; +using Convey.MessageBrokers; + +namespace MiniSpace.Services.MediaFiles.Application.Events.External +{ + [Message("events")] + public class EventCreated : IEvent + { + public Guid EventId { get; } + public IEnumerable MediaFilesIds { get; } + + public EventCreated(Guid eventId, IEnumerable mediaFilesIds) + { + EventId = eventId; + MediaFilesIds = mediaFilesIds; + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Events/External/EventDeleted.cs b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Events/External/EventDeleted.cs new file mode 100644 index 000000000..a47fa908f --- /dev/null +++ b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Events/External/EventDeleted.cs @@ -0,0 +1,16 @@ +using Convey.CQRS.Events; +using Convey.MessageBrokers; + +namespace MiniSpace.Services.MediaFiles.Application.Events.External +{ + [Message("events")] + public class EventDeleted : IEvent + { + public Guid EventId { get; } + + public EventDeleted(Guid eventId) + { + EventId = eventId; + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Events/External/EventUpdated.cs b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Events/External/EventUpdated.cs new file mode 100644 index 000000000..b333bd6fe --- /dev/null +++ b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Events/External/EventUpdated.cs @@ -0,0 +1,18 @@ +using Convey.CQRS.Events; +using Convey.MessageBrokers; + +namespace MiniSpace.Services.MediaFiles.Application.Events.External +{ + [Message("events")] + public class EventUpdated : IEvent + { + public Guid EventId { get; } + public IEnumerable MediaFilesIds { get; } + + public EventUpdated(Guid eventId, IEnumerable mediaFilesIds) + { + EventId = eventId; + MediaFilesIds = mediaFilesIds; + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Events/External/Handlers/EventCreatedHandler.cs b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Events/External/Handlers/EventCreatedHandler.cs new file mode 100644 index 000000000..d27af87e5 --- /dev/null +++ b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Events/External/Handlers/EventCreatedHandler.cs @@ -0,0 +1,32 @@ +using Convey.CQRS.Commands; +using Convey.CQRS.Events; +using MiniSpace.Services.MediaFiles.Application.Commands; +using MiniSpace.Services.MediaFiles.Core.Entities; +using MiniSpace.Services.MediaFiles.Core.Repositories; + +namespace MiniSpace.Services.MediaFiles.Application.Events.External.Handlers +{ + public class EventCreatedHandler : IEventHandler + { + private readonly IFileSourceInfoRepository _fileSourceInfoRepository; + + public EventCreatedHandler(IFileSourceInfoRepository fileSourceInfoRepository) + { + _fileSourceInfoRepository = fileSourceInfoRepository; + } + + public async Task HandleAsync(EventCreated @event, CancellationToken cancellationToken) + { + var fileSourceInfos = + await _fileSourceInfoRepository.FindAsync(@event.EventId, ContextType.Event); + foreach (var fileSourceInfo in fileSourceInfos) + { + if(@event.MediaFilesIds.Contains(fileSourceInfo.Id)) + { + fileSourceInfo.Associate(); + await _fileSourceInfoRepository.UpdateAsync(fileSourceInfo); + } + } + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Events/External/Handlers/EventDeletedHandler.cs b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Events/External/Handlers/EventDeletedHandler.cs new file mode 100644 index 000000000..b007b0ae3 --- /dev/null +++ b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Events/External/Handlers/EventDeletedHandler.cs @@ -0,0 +1,31 @@ +using Convey.CQRS.Commands; +using Convey.CQRS.Events; +using MiniSpace.Services.MediaFiles.Application.Commands; +using MiniSpace.Services.MediaFiles.Core.Entities; +using MiniSpace.Services.MediaFiles.Core.Repositories; + +namespace MiniSpace.Services.MediaFiles.Application.Events.External.Handlers +{ + public class EventDeletedHandler : IEventHandler + { + private readonly IFileSourceInfoRepository _fileSourceInfoRepository; + private readonly ICommandDispatcher _commandDispatcher; + + public EventDeletedHandler(IFileSourceInfoRepository fileSourceInfoRepository, ICommandDispatcher commandDispatcher) + { + _fileSourceInfoRepository = fileSourceInfoRepository; + _commandDispatcher = commandDispatcher; + } + + public async Task HandleAsync(EventDeleted @event, CancellationToken cancellationToken) + { + var fileSourceInfos = + await _fileSourceInfoRepository.FindAsync(@event.EventId, ContextType.Event); + foreach (var fileSourceInfo in fileSourceInfos) + { + fileSourceInfo.Unassociate(); + await _fileSourceInfoRepository.UpdateAsync(fileSourceInfo); + } + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Events/External/Handlers/EventUpdatedHandler.cs b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Events/External/Handlers/EventUpdatedHandler.cs new file mode 100644 index 000000000..060861ca0 --- /dev/null +++ b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Events/External/Handlers/EventUpdatedHandler.cs @@ -0,0 +1,38 @@ +using Convey.CQRS.Commands; +using Convey.CQRS.Events; +using MiniSpace.Services.MediaFiles.Application.Commands; +using MiniSpace.Services.MediaFiles.Core.Entities; +using MiniSpace.Services.MediaFiles.Core.Repositories; + +namespace MiniSpace.Services.MediaFiles.Application.Events.External.Handlers +{ + public class EventUpdatedHandler : IEventHandler + { + private readonly IFileSourceInfoRepository _fileSourceInfoRepository; + private readonly ICommandDispatcher _commandDispatcher; + + public EventUpdatedHandler(IFileSourceInfoRepository fileSourceInfoRepository, ICommandDispatcher commandDispatcher) + { + _fileSourceInfoRepository = fileSourceInfoRepository; + _commandDispatcher = commandDispatcher; + } + + public async Task HandleAsync(EventUpdated @event, CancellationToken cancellationToken) + { + var fileSourceInfos = + await _fileSourceInfoRepository.FindAsync(@event.EventId, ContextType.Event); + foreach (var fileSourceInfo in fileSourceInfos) + { + if(@event.MediaFilesIds.Contains(fileSourceInfo.Id)) + { + fileSourceInfo.Associate(); + } + else + { + fileSourceInfo.Unassociate(); + } + await _fileSourceInfoRepository.UpdateAsync(fileSourceInfo); + } + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Events/External/Handlers/PostCreatedHandler.cs b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Events/External/Handlers/PostCreatedHandler.cs new file mode 100644 index 000000000..fa18b9ced --- /dev/null +++ b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Events/External/Handlers/PostCreatedHandler.cs @@ -0,0 +1,34 @@ +using Convey.CQRS.Commands; +using Convey.CQRS.Events; +using MiniSpace.Services.MediaFiles.Application.Commands; +using MiniSpace.Services.MediaFiles.Core.Entities; +using MiniSpace.Services.MediaFiles.Core.Repositories; + +namespace MiniSpace.Services.MediaFiles.Application.Events.External.Handlers +{ + public class PostCreatedHandler : IEventHandler + { + private readonly IFileSourceInfoRepository _fileSourceInfoRepository; + private readonly ICommandDispatcher _commandDispatcher; + + public PostCreatedHandler(IFileSourceInfoRepository fileSourceInfoRepository, ICommandDispatcher commandDispatcher) + { + _fileSourceInfoRepository = fileSourceInfoRepository; + _commandDispatcher = commandDispatcher; + } + + public async Task HandleAsync(PostCreated @event, CancellationToken cancellationToken) + { + var fileSourceInfos = + await _fileSourceInfoRepository.FindAsync(@event.PostId, ContextType.Post); + foreach (var fileSourceInfo in fileSourceInfos) + { + if(@event.MediaFilesIds.Contains(fileSourceInfo.Id)) + { + fileSourceInfo.Associate(); + await _fileSourceInfoRepository.UpdateAsync(fileSourceInfo); + } + } + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Events/External/Handlers/PostDeletedHandler.cs b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Events/External/Handlers/PostDeletedHandler.cs new file mode 100644 index 000000000..4c0c6a592 --- /dev/null +++ b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Events/External/Handlers/PostDeletedHandler.cs @@ -0,0 +1,31 @@ +using Convey.CQRS.Commands; +using Convey.CQRS.Events; +using MiniSpace.Services.MediaFiles.Application.Commands; +using MiniSpace.Services.MediaFiles.Core.Entities; +using MiniSpace.Services.MediaFiles.Core.Repositories; + +namespace MiniSpace.Services.MediaFiles.Application.Events.External.Handlers +{ + public class PostDeletedHandler : IEventHandler + { + private readonly IFileSourceInfoRepository _fileSourceInfoRepository; + private readonly ICommandDispatcher _commandDispatcher; + + public PostDeletedHandler(IFileSourceInfoRepository fileSourceInfoRepository, ICommandDispatcher commandDispatcher) + { + _fileSourceInfoRepository = fileSourceInfoRepository; + _commandDispatcher = commandDispatcher; + } + + public async Task HandleAsync(PostDeleted @event, CancellationToken cancellationToken) + { + var fileSourceInfos = + await _fileSourceInfoRepository.FindAsync(@event.PostId, ContextType.Post); + foreach (var fileSourceInfo in fileSourceInfos) + { + fileSourceInfo.Unassociate(); + await _fileSourceInfoRepository.UpdateAsync(fileSourceInfo); + } + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Events/External/Handlers/PostUpdatedHandler.cs b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Events/External/Handlers/PostUpdatedHandler.cs new file mode 100644 index 000000000..7f7732ac2 --- /dev/null +++ b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Events/External/Handlers/PostUpdatedHandler.cs @@ -0,0 +1,38 @@ +using Convey.CQRS.Commands; +using Convey.CQRS.Events; +using MiniSpace.Services.MediaFiles.Application.Commands; +using MiniSpace.Services.MediaFiles.Core.Entities; +using MiniSpace.Services.MediaFiles.Core.Repositories; + +namespace MiniSpace.Services.MediaFiles.Application.Events.External.Handlers +{ + public class PostUpdatedHandler : IEventHandler + { + private readonly IFileSourceInfoRepository _fileSourceInfoRepository; + private readonly ICommandDispatcher _commandDispatcher; + + public PostUpdatedHandler(IFileSourceInfoRepository fileSourceInfoRepository, ICommandDispatcher commandDispatcher) + { + _fileSourceInfoRepository = fileSourceInfoRepository; + _commandDispatcher = commandDispatcher; + } + + public async Task HandleAsync(PostUpdated @event, CancellationToken cancellationToken) + { + var fileSourceInfos = + await _fileSourceInfoRepository.FindAsync(@event.PostId, ContextType.Post); + foreach (var fileSourceInfo in fileSourceInfos) + { + if(@event.MediaFilesIds.Contains(fileSourceInfo.Id)) + { + fileSourceInfo.Associate(); + } + else + { + fileSourceInfo.Unassociate(); + } + await _fileSourceInfoRepository.UpdateAsync(fileSourceInfo); + } + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Events/External/Handlers/StudentCreatedHandler.cs b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Events/External/Handlers/StudentCreatedHandler.cs new file mode 100644 index 000000000..0e1050a04 --- /dev/null +++ b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Events/External/Handlers/StudentCreatedHandler.cs @@ -0,0 +1,34 @@ +using Convey.CQRS.Commands; +using Convey.CQRS.Events; +using MiniSpace.Services.MediaFiles.Application.Commands; +using MiniSpace.Services.MediaFiles.Core.Entities; +using MiniSpace.Services.MediaFiles.Core.Repositories; + +namespace MiniSpace.Services.MediaFiles.Application.Events.External.Handlers +{ + public class StudentCreatedHandler : IEventHandler + { + private readonly IFileSourceInfoRepository _fileSourceInfoRepository; + private readonly ICommandDispatcher _commandDispatcher; + + public StudentCreatedHandler(IFileSourceInfoRepository fileSourceInfoRepository, ICommandDispatcher commandDispatcher) + { + _fileSourceInfoRepository = fileSourceInfoRepository; + _commandDispatcher = commandDispatcher; + } + + public async Task HandleAsync(StudentCreated @event, CancellationToken cancellationToken) + { + var fileSourceInfos = + await _fileSourceInfoRepository.FindAsync(@event.StudentId, ContextType.StudentProfile); + foreach (var fileSourceInfo in fileSourceInfos) + { + if (fileSourceInfo.Id == @event.MediaFileId) + { + fileSourceInfo.Associate(); + await _fileSourceInfoRepository.UpdateAsync(fileSourceInfo); + } + } + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Events/External/Handlers/StudentDeletedHandler.cs b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Events/External/Handlers/StudentDeletedHandler.cs new file mode 100644 index 000000000..dfbf9cf71 --- /dev/null +++ b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Events/External/Handlers/StudentDeletedHandler.cs @@ -0,0 +1,31 @@ +using Convey.CQRS.Commands; +using Convey.CQRS.Events; +using MiniSpace.Services.MediaFiles.Application.Commands; +using MiniSpace.Services.MediaFiles.Core.Entities; +using MiniSpace.Services.MediaFiles.Core.Repositories; + +namespace MiniSpace.Services.MediaFiles.Application.Events.External.Handlers +{ + public class StudentDeletedHandler : IEventHandler + { + private readonly IFileSourceInfoRepository _fileSourceInfoRepository; + private readonly ICommandDispatcher _commandDispatcher; + + public StudentDeletedHandler(IFileSourceInfoRepository fileSourceInfoRepository, ICommandDispatcher commandDispatcher) + { + _fileSourceInfoRepository = fileSourceInfoRepository; + _commandDispatcher = commandDispatcher; + } + + public async Task HandleAsync(StudentDeleted @event, CancellationToken cancellationToken) + { + var fileSourceInfos = + await _fileSourceInfoRepository.FindAsync(@event.StudentId, ContextType.StudentProfile); + foreach (var fileSourceInfo in fileSourceInfos) + { + fileSourceInfo.Unassociate(); + await _fileSourceInfoRepository.UpdateAsync(fileSourceInfo); + } + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Events/External/Handlers/StudentUpdatedHandler.cs b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Events/External/Handlers/StudentUpdatedHandler.cs new file mode 100644 index 000000000..e630a5f8b --- /dev/null +++ b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Events/External/Handlers/StudentUpdatedHandler.cs @@ -0,0 +1,38 @@ +using Convey.CQRS.Commands; +using Convey.CQRS.Events; +using MiniSpace.Services.MediaFiles.Application.Commands; +using MiniSpace.Services.MediaFiles.Core.Entities; +using MiniSpace.Services.MediaFiles.Core.Repositories; + +namespace MiniSpace.Services.MediaFiles.Application.Events.External.Handlers +{ + public class StudentUpdatedHandler : IEventHandler + { + private readonly IFileSourceInfoRepository _fileSourceInfoRepository; + private readonly ICommandDispatcher _commandDispatcher; + + public StudentUpdatedHandler(IFileSourceInfoRepository fileSourceInfoRepository, ICommandDispatcher commandDispatcher) + { + _fileSourceInfoRepository = fileSourceInfoRepository; + _commandDispatcher = commandDispatcher; + } + + public async Task HandleAsync(StudentUpdated @event, CancellationToken cancellationToken) + { + var fileSourceInfos = + await _fileSourceInfoRepository.FindAsync(@event.StudentId, ContextType.StudentProfile); + foreach (var fileSourceInfo in fileSourceInfos) + { + if (fileSourceInfo.Id == @event.MediaFileId) + { + fileSourceInfo.Associate(); + } + else + { + fileSourceInfo.Unassociate(); + } + await _fileSourceInfoRepository.UpdateAsync(fileSourceInfo); + } + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Events/External/PostCreated.cs b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Events/External/PostCreated.cs new file mode 100644 index 000000000..2c54d7f29 --- /dev/null +++ b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Events/External/PostCreated.cs @@ -0,0 +1,18 @@ +using Convey.CQRS.Events; +using Convey.MessageBrokers; + +namespace MiniSpace.Services.MediaFiles.Application.Events.External +{ + [Message("posts")] + public class PostCreated : IEvent + { + public Guid PostId { get; } + public IEnumerable MediaFilesIds { get; } + + public PostCreated(Guid postId, IEnumerable mediaFilesIds) + { + PostId = postId; + MediaFilesIds = mediaFilesIds; + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Events/External/PostDeleted.cs b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Events/External/PostDeleted.cs new file mode 100644 index 000000000..210b1542f --- /dev/null +++ b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Events/External/PostDeleted.cs @@ -0,0 +1,16 @@ +using Convey.CQRS.Events; +using Convey.MessageBrokers; + +namespace MiniSpace.Services.MediaFiles.Application.Events.External +{ + [Message("posts")] + public class PostDeleted : IEvent + { + public Guid PostId { get; } + + public PostDeleted(Guid postId) + { + PostId = postId; + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Events/External/PostUpdated.cs b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Events/External/PostUpdated.cs new file mode 100644 index 000000000..a162cf8fa --- /dev/null +++ b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Events/External/PostUpdated.cs @@ -0,0 +1,18 @@ +using Convey.CQRS.Events; +using Convey.MessageBrokers; + +namespace MiniSpace.Services.MediaFiles.Application.Events.External +{ + [Message("posts")] + public class PostUpdated : IEvent + { + public Guid PostId { get; } + public IEnumerable MediaFilesIds { get; } + + public PostUpdated(Guid postId, IEnumerable mediaFilesIds) + { + PostId = postId; + MediaFilesIds = mediaFilesIds; + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Events/External/StudentCreated.cs b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Events/External/StudentCreated.cs new file mode 100644 index 000000000..0726ee120 --- /dev/null +++ b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Events/External/StudentCreated.cs @@ -0,0 +1,18 @@ +using Convey.CQRS.Events; +using Convey.MessageBrokers; + +namespace MiniSpace.Services.MediaFiles.Application.Events.External +{ + [Message("students")] + public class StudentCreated : IEvent + { + public Guid StudentId { get; } + public Guid MediaFileId { get; } + + public StudentCreated(Guid studentId, Guid mediaFileId) + { + StudentId = studentId; + MediaFileId = mediaFileId; + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Events/External/StudentDeleted.cs b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Events/External/StudentDeleted.cs new file mode 100644 index 000000000..5c666e72d --- /dev/null +++ b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Events/External/StudentDeleted.cs @@ -0,0 +1,16 @@ +using Convey.CQRS.Events; +using Convey.MessageBrokers; + +namespace MiniSpace.Services.MediaFiles.Application.Events.External +{ + [Message("students")] + public class StudentDeleted : IEvent + { + public Guid StudentId { get; } + + public StudentDeleted(Guid studentId) + { + StudentId = studentId; + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Events/External/StudentUpdated.cs b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Events/External/StudentUpdated.cs new file mode 100644 index 000000000..43ebef223 --- /dev/null +++ b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Events/External/StudentUpdated.cs @@ -0,0 +1,18 @@ +using Convey.CQRS.Events; +using Convey.MessageBrokers; + +namespace MiniSpace.Services.MediaFiles.Application.Events.External +{ + [Message("students")] + public class StudentUpdated : IEvent + { + public Guid StudentId { get; } + public Guid MediaFileId { get; } + + public StudentUpdated(Guid studentId, Guid mediaFileId) + { + StudentId = studentId; + MediaFileId = mediaFileId; + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Events/FileCleanupBackgroundWorkerStarted.cs b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Events/FileCleanupBackgroundWorkerStarted.cs new file mode 100644 index 000000000..4781c5057 --- /dev/null +++ b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Events/FileCleanupBackgroundWorkerStarted.cs @@ -0,0 +1,14 @@ +using Convey.CQRS.Events; + +namespace MiniSpace.Services.MediaFiles.Application.Events +{ + public class FileCleanupBackgroundWorkerStarted: IEvent + { + public DateTime StartedAt { get; } + + public FileCleanupBackgroundWorkerStarted(DateTime startedAt) + { + StartedAt = startedAt; + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Events/FileCleanupBackgroundWorkerStopped.cs b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Events/FileCleanupBackgroundWorkerStopped.cs new file mode 100644 index 000000000..5bcd641fc --- /dev/null +++ b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Events/FileCleanupBackgroundWorkerStopped.cs @@ -0,0 +1,14 @@ +using Convey.CQRS.Events; + +namespace MiniSpace.Services.MediaFiles.Application.Events +{ + public class FileCleanupBackgroundWorkerStopped: IEvent + { + public DateTime StoppedAt { get; } + + public FileCleanupBackgroundWorkerStopped(DateTime stoppedAt) + { + StoppedAt = stoppedAt; + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Events/MediaFileDeleted.cs b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Events/MediaFileDeleted.cs new file mode 100644 index 000000000..a7eb41a84 --- /dev/null +++ b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Events/MediaFileDeleted.cs @@ -0,0 +1,18 @@ +using Convey.CQRS.Events; + +namespace MiniSpace.Services.MediaFiles.Application.Events +{ + public class MediaFileDeleted: IEvent + { + public Guid MediaFileId { get; } + public Guid SourceId { get; } + public string Source { get; } + + public MediaFileDeleted(Guid mediaFileId, Guid sourceId, string source) + { + MediaFileId = mediaFileId; + SourceId = sourceId; + Source = source; + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Events/MediaFileUploaded.cs b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Events/MediaFileUploaded.cs new file mode 100644 index 000000000..5f2cf96ad --- /dev/null +++ b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Events/MediaFileUploaded.cs @@ -0,0 +1,16 @@ +using Convey.CQRS.Events; + +namespace MiniSpace.Services.MediaFiles.Application.Events +{ + public class MediaFileUploaded : IEvent + { + public Guid Id { get; } + public string FileName { get; } + + public MediaFileUploaded(Guid id, string fileName) + { + Id = id; + FileName = fileName; + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Events/UnassociatedFilesCleaned.cs b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Events/UnassociatedFilesCleaned.cs new file mode 100644 index 000000000..757821ca7 --- /dev/null +++ b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Events/UnassociatedFilesCleaned.cs @@ -0,0 +1,14 @@ +using Convey.CQRS.Events; + +namespace MiniSpace.Services.MediaFiles.Application.Events +{ + public class UnassociatedFilesCleaned: IEvent + { + public DateTime OccurredAt { get; } + + public UnassociatedFilesCleaned(DateTime occurredAt) + { + OccurredAt = occurredAt; + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Exceptions/AppException.cs b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Exceptions/AppException.cs new file mode 100644 index 000000000..4447155a7 --- /dev/null +++ b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Exceptions/AppException.cs @@ -0,0 +1,11 @@ +namespace MiniSpace.Services.MediaFiles.Application.Exceptions +{ + public class AppException : Exception + { + public virtual string Code { get; } + + protected AppException(string message) : base(message) + { + } + } +} diff --git a/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Exceptions/FileTypeDoesNotMatchContentTypeException.cs b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Exceptions/FileTypeDoesNotMatchContentTypeException.cs new file mode 100644 index 000000000..942623598 --- /dev/null +++ b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Exceptions/FileTypeDoesNotMatchContentTypeException.cs @@ -0,0 +1,16 @@ +namespace MiniSpace.Services.MediaFiles.Application.Exceptions +{ + public class FileTypeDoesNotMatchContentTypeException : AppException + { + public override string Code { get; } = "file_type_does_not_match_content_type"; + public string FileType { get; } + public string ContentType { get; } + + public FileTypeDoesNotMatchContentTypeException(string fileType, string contentType) + : base($"File extension: {fileType} is not matching content type: {contentType}") + { + FileType = fileType; + ContentType = contentType; + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Exceptions/InvalidContextTypeException.cs b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Exceptions/InvalidContextTypeException.cs new file mode 100644 index 000000000..9efdbe128 --- /dev/null +++ b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Exceptions/InvalidContextTypeException.cs @@ -0,0 +1,13 @@ +namespace MiniSpace.Services.MediaFiles.Application.Exceptions +{ + public class InvalidContextTypeException : AppException + { + public override string Code { get; } = "invalid_context_type"; + public string ContextType { get; } + + public InvalidContextTypeException(string contextType) : base($"Invalid context type: {contextType}.") + { + ContextType = contextType; + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Exceptions/InvalidFileContentTypeException.cs b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Exceptions/InvalidFileContentTypeException.cs new file mode 100644 index 000000000..7afb2e5d4 --- /dev/null +++ b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Exceptions/InvalidFileContentTypeException.cs @@ -0,0 +1,13 @@ +namespace MiniSpace.Services.MediaFiles.Application.Exceptions +{ + public class InvalidFileContentTypeException : AppException + { + public override string Code { get; } = "invalid_file_content_type"; + public string ContentType { get; } + + public InvalidFileContentTypeException(string contentType) : base($"Invalid file content type: {contentType}.") + { + ContentType = contentType; + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Exceptions/InvalidFileSizeException.cs b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Exceptions/InvalidFileSizeException.cs new file mode 100644 index 000000000..e570dec7c --- /dev/null +++ b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Exceptions/InvalidFileSizeException.cs @@ -0,0 +1,16 @@ +namespace MiniSpace.Services.MediaFiles.Application.Exceptions +{ + public class InvalidFileSizeException : AppException + { + public override string Code { get; } = "invalid_file_size"; + public int FileSize { get; } + public int MaxFileSize { get; } + + public InvalidFileSizeException(int fileSize, int maxFileSize) + : base($"Invalid file size: {fileSize}. Maximum valid file size: {maxFileSize}.") + { + FileSize = fileSize; + MaxFileSize = maxFileSize; + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Exceptions/MediaFileNotFoundException.cs b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Exceptions/MediaFileNotFoundException.cs new file mode 100644 index 000000000..9c9bd144a --- /dev/null +++ b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Exceptions/MediaFileNotFoundException.cs @@ -0,0 +1,14 @@ +namespace MiniSpace.Services.MediaFiles.Application.Exceptions +{ + public class MediaFileNotFoundException: AppException + { + public override string Code { get; } = "media_file_not_found"; + public Guid MediaFileId { get; } + + public MediaFileNotFoundException(Guid mediaFileId) + : base($"Media file with ID: {mediaFileId} was not found.") + { + MediaFileId = mediaFileId; + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Exceptions/UnauthorizedMediaFileAccessException.cs b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Exceptions/UnauthorizedMediaFileAccessException.cs new file mode 100644 index 000000000..e0b0e9395 --- /dev/null +++ b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Exceptions/UnauthorizedMediaFileAccessException.cs @@ -0,0 +1,18 @@ +namespace MiniSpace.Services.MediaFiles.Application.Exceptions +{ + public class UnauthorizedMediaFileAccessException: AppException + { + public override string Code { get; } = "unauthorized_media_file_access"; + public Guid MediaFileId { get; } + public Guid UserId { get; } + public Guid UploaderId { get; } + + public UnauthorizedMediaFileAccessException(Guid mediaFileId, Guid userId, Guid uploaderId) + : base($"User with ID: {userId} tried to access media file with ID: {mediaFileId} uploaded by user with ID: {uploaderId}.") + { + MediaFileId = mediaFileId; + UserId = userId; + UploaderId = uploaderId; + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Exceptions/UnauthorizedMediaFileUploadException.cs b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Exceptions/UnauthorizedMediaFileUploadException.cs new file mode 100644 index 000000000..91ad23b02 --- /dev/null +++ b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Exceptions/UnauthorizedMediaFileUploadException.cs @@ -0,0 +1,16 @@ +namespace MiniSpace.Services.MediaFiles.Application.Exceptions +{ + public class UnauthorizedMediaFileUploadException : AppException + { + public override string Code { get; } = "unauthorized_media_file_upload"; + public Guid IdentityId { get; } + public Guid UploaderId { get; } + + public UnauthorizedMediaFileUploadException(Guid identityId, Guid uploaderId) + : base($"User with ID: {uploaderId} is not authorized to upload media files. Identity ID: {identityId}.") + { + IdentityId = identityId; + UploaderId = uploaderId; + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Extensions.cs b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Extensions.cs new file mode 100644 index 000000000..6553ffbae --- /dev/null +++ b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Extensions.cs @@ -0,0 +1,16 @@ +using Convey; +using Convey.CQRS.Commands; +using Convey.CQRS.Events; + +namespace MiniSpace.Services.MediaFiles.Application +{ + public static class Extensions + { + public static IConveyBuilder AddApplication(this IConveyBuilder builder) + => builder + .AddCommandHandlers() + .AddEventHandlers() + .AddInMemoryCommandDispatcher() + .AddInMemoryEventDispatcher(); + } +} diff --git a/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/IAppContext.cs b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/IAppContext.cs new file mode 100644 index 000000000..d3084031f --- /dev/null +++ b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/IAppContext.cs @@ -0,0 +1,8 @@ +namespace MiniSpace.Services.MediaFiles.Application +{ + public interface IAppContext + { + string RequestId { get; } + IIdentityContext Identity { get; } + } +} diff --git a/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/IIdentityContext.cs b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/IIdentityContext.cs new file mode 100644 index 000000000..b0748b5e8 --- /dev/null +++ b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/IIdentityContext.cs @@ -0,0 +1,15 @@ +namespace MiniSpace.Services.MediaFiles.Application +{ + public interface IIdentityContext + { + Guid Id { get; } + string Role { get; } + string Name { get; } + string Email { get; } + bool IsAuthenticated { get; } + bool IsAdmin { get; } + bool IsBanned { get; } + bool IsOrganizer { get; } + IDictionary Claims { get; } + } +} diff --git a/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/MiniSpace.Services.MediaFiles.Application.csproj b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/MiniSpace.Services.MediaFiles.Application.csproj new file mode 100644 index 000000000..a4867130e --- /dev/null +++ b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/MiniSpace.Services.MediaFiles.Application.csproj @@ -0,0 +1,23 @@ + + + + net8.0 + enable + disable + + + + + + + + + + + + + + + + + diff --git a/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Queries/GetMediaFile.cs b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Queries/GetMediaFile.cs new file mode 100644 index 000000000..06ad3f327 --- /dev/null +++ b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Queries/GetMediaFile.cs @@ -0,0 +1,11 @@ +using Convey.CQRS.Queries; +using Microsoft.AspNetCore.Mvc; +using MiniSpace.Services.MediaFiles.Application.Dto; + +namespace MiniSpace.Services.MediaFiles.Application.Queries +{ + public class GetMediaFile : IQuery + { + public Guid MediaFileId { get; set; } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Queries/GetOriginalMediaFile.cs b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Queries/GetOriginalMediaFile.cs new file mode 100644 index 000000000..09c9fdf65 --- /dev/null +++ b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Queries/GetOriginalMediaFile.cs @@ -0,0 +1,10 @@ +using Convey.CQRS.Queries; +using MiniSpace.Services.MediaFiles.Application.Dto; + +namespace MiniSpace.Services.MediaFiles.Application.Queries +{ + public class GetOriginalMediaFile : IQuery + { + public Guid MediaFileId { get; set; } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Services/IDateTimeProvider.cs b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Services/IDateTimeProvider.cs new file mode 100644 index 000000000..9cb5354b8 --- /dev/null +++ b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Services/IDateTimeProvider.cs @@ -0,0 +1,7 @@ +namespace MiniSpace.Services.MediaFiles.Application.Services +{ + public interface IDateTimeProvider + { + DateTime Now { get; } + } +} diff --git a/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Services/IEventMapper.cs b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Services/IEventMapper.cs new file mode 100644 index 000000000..b26bb7e2e --- /dev/null +++ b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Services/IEventMapper.cs @@ -0,0 +1,11 @@ +using Convey.CQRS.Events; +using MiniSpace.Services.MediaFiles.Core.Events; + +namespace MiniSpace.Services.MediaFiles.Application.Services +{ + public interface IEventMapper + { + IEvent Map(IDomainEvent @event); + IEnumerable MapAll(IEnumerable events); + } +} diff --git a/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Services/IFileValidator.cs b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Services/IFileValidator.cs new file mode 100644 index 000000000..b6450def4 --- /dev/null +++ b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Services/IFileValidator.cs @@ -0,0 +1,8 @@ +namespace MiniSpace.Services.MediaFiles.Application.Services +{ + public interface IFileValidator + { + public void ValidateFileSize(int size); + public void ValidateFileExtensions(byte[] bytes, string contentType); + } +} \ No newline at end of file diff --git a/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Services/IGridFSService.cs b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Services/IGridFSService.cs new file mode 100644 index 000000000..0f40c2100 --- /dev/null +++ b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Services/IGridFSService.cs @@ -0,0 +1,11 @@ +using MongoDB.Bson; + +namespace MiniSpace.Services.MediaFiles.Application.Services +{ + public interface IGridFSService + { + Task UploadFileAsync(string fileName, Stream fileStream); + Task DownloadFileAsync(ObjectId fileId, Stream destination); + Task DeleteFileAsync(ObjectId fileId); + } +} \ No newline at end of file diff --git a/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Services/IMediaFilesService.cs b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Services/IMediaFilesService.cs new file mode 100644 index 000000000..1a3893fd1 --- /dev/null +++ b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Services/IMediaFilesService.cs @@ -0,0 +1,10 @@ +using MiniSpace.Services.MediaFiles.Application.Commands; +using MiniSpace.Services.MediaFiles.Application.Dto; + +namespace MiniSpace.Services.MediaFiles.Application.Services +{ + public interface IMediaFilesService + { + public Task UploadAsync(UploadMediaFile command); + } +} \ No newline at end of file diff --git a/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Services/IMessageBroker.cs b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Services/IMessageBroker.cs new file mode 100644 index 000000000..2af117300 --- /dev/null +++ b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Application/Services/IMessageBroker.cs @@ -0,0 +1,10 @@ +using Convey.CQRS.Events; + +namespace MiniSpace.Services.MediaFiles.Application.Services +{ + public interface IMessageBroker + { + Task PublishAsync(params IEvent[] events); + Task PublishAsync(IEnumerable events); + } +} diff --git a/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Core/Entities/AggregateId.cs b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Core/Entities/AggregateId.cs new file mode 100644 index 000000000..922491499 --- /dev/null +++ b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Core/Entities/AggregateId.cs @@ -0,0 +1,50 @@ +using MiniSpace.Services.MediaFiles.Core.Exceptions; + +namespace MiniSpace.Services.MediaFiles.Core.Entities +{ + public class AggregateId : IEquatable + { + public Guid Value { get; } + + public AggregateId() + { + Value = Guid.NewGuid(); + } + + public AggregateId(Guid value) + { + if (value == Guid.Empty) + { + throw new InvalidAggregateIdException(); + } + + Value = value; + } + + public bool Equals(AggregateId other) + { + if (ReferenceEquals(null, other)) return false; + return ReferenceEquals(this, other) || Value.Equals(other.Value); + } + + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) return false; + if (ReferenceEquals(this, obj)) return true; + return obj.GetType() == GetType() && Equals((AggregateId) obj); + } + + public override int GetHashCode() + { + return Value.GetHashCode(); + } + + public static implicit operator Guid(AggregateId id) + => id.Value; + + public static implicit operator AggregateId(Guid id) + => new AggregateId(id); + + public override string ToString() => Value.ToString(); + } +} diff --git a/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Core/Entities/AggregateRoot.cs b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Core/Entities/AggregateRoot.cs new file mode 100644 index 000000000..fca21a6dd --- /dev/null +++ b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Core/Entities/AggregateRoot.cs @@ -0,0 +1,19 @@ +using MiniSpace.Services.MediaFiles.Core.Events; + +namespace MiniSpace.Services.MediaFiles.Core.Entities +{ + public abstract class AggregateRoot + { + private readonly List _events = new List(); + public IEnumerable Events => _events; + public AggregateId Id { get; protected set; } + public int Version { get; protected set; } + + protected void AddEvent(IDomainEvent @event) + { + _events.Add(@event); + } + + public void ClearEvents() => _events.Clear(); + } +} diff --git a/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Core/Entities/ContextType.cs b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Core/Entities/ContextType.cs new file mode 100644 index 000000000..c93a71bd3 --- /dev/null +++ b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Core/Entities/ContextType.cs @@ -0,0 +1,9 @@ +namespace MiniSpace.Services.MediaFiles.Core.Entities +{ + public enum ContextType + { + Event, + Post, + StudentProfile, + } +} \ No newline at end of file diff --git a/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Core/Entities/FileSourceInfo.cs b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Core/Entities/FileSourceInfo.cs new file mode 100644 index 000000000..84e52e5c5 --- /dev/null +++ b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Core/Entities/FileSourceInfo.cs @@ -0,0 +1,42 @@ +using MongoDB.Bson; + +namespace MiniSpace.Services.MediaFiles.Core.Entities +{ + public class FileSourceInfo: AggregateRoot + { + public Guid SourceId { get; set; } + public ContextType SourceType { get; set; } + public Guid UploaderId { get; set; } + public State State { get; set; } + public DateTime CreatedAt { get; set; } + public ObjectId OriginalFileId { get; set; } + public string OriginalFileContentType { get; set; } + public ObjectId FileId { get; set; } + public string FileName { get; set; } + + public FileSourceInfo(Guid id, Guid sourceId, ContextType sourceType, Guid uploaderId, State state, + DateTime createdAt, ObjectId originalFileId, string originalFileContentType, ObjectId fileId, string fileName) + { + Id = id; + SourceId = sourceId; + SourceType = sourceType; + UploaderId = uploaderId; + State = state; + CreatedAt = createdAt; + OriginalFileId = originalFileId; + OriginalFileContentType = originalFileContentType; + FileId = fileId; + FileName = fileName; + } + + public void Associate() + { + State = State.Associated; + } + + public void Unassociate() + { + State = State.Unassociated; + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Core/Entities/State.cs b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Core/Entities/State.cs new file mode 100644 index 000000000..45aa55612 --- /dev/null +++ b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Core/Entities/State.cs @@ -0,0 +1,8 @@ +namespace MiniSpace.Services.MediaFiles.Core.Entities +{ + public enum State + { + Associated, + Unassociated + } +} \ No newline at end of file diff --git a/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Core/Events/IDomainEvent.cs b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Core/Events/IDomainEvent.cs new file mode 100644 index 000000000..ecf21afe6 --- /dev/null +++ b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Core/Events/IDomainEvent.cs @@ -0,0 +1,7 @@ +namespace MiniSpace.Services.MediaFiles.Core.Events +{ + public interface IDomainEvent + { + + } +} diff --git a/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Core/Exceptions/DomainException.cs b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Core/Exceptions/DomainException.cs new file mode 100644 index 000000000..cc1f19637 --- /dev/null +++ b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Core/Exceptions/DomainException.cs @@ -0,0 +1,11 @@ +namespace MiniSpace.Services.MediaFiles.Core.Exceptions +{ + public abstract class DomainException : Exception + { + public virtual string Code { get; } + + protected DomainException(string message) : base(message) + { + } + } +} diff --git a/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Core/Exceptions/InvalidAggregateIdException.cs b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Core/Exceptions/InvalidAggregateIdException.cs new file mode 100644 index 000000000..376f91b1a --- /dev/null +++ b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Core/Exceptions/InvalidAggregateIdException.cs @@ -0,0 +1,11 @@ +namespace MiniSpace.Services.MediaFiles.Core.Exceptions +{ + public class InvalidAggregateIdException : DomainException + { + public override string Code { get; } = "invalid_aggregate_id"; + + public InvalidAggregateIdException() : base($"Invalid aggregate id.") + { + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Core/MiniSpace.Services.MediaFiles.Core.csproj b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Core/MiniSpace.Services.MediaFiles.Core.csproj new file mode 100644 index 000000000..4e485b559 --- /dev/null +++ b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Core/MiniSpace.Services.MediaFiles.Core.csproj @@ -0,0 +1,13 @@ + + + + net8.0 + enable + disable + + + + + + + diff --git a/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Core/Repositories/IFileSourceInfoRepository.cs b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Core/Repositories/IFileSourceInfoRepository.cs new file mode 100644 index 000000000..5095eadc5 --- /dev/null +++ b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Core/Repositories/IFileSourceInfoRepository.cs @@ -0,0 +1,15 @@ +using MiniSpace.Services.MediaFiles.Core.Entities; + +namespace MiniSpace.Services.MediaFiles.Core.Repositories +{ + public interface IFileSourceInfoRepository + { + Task GetAsync(Guid id); + Task> GetAllUnassociatedAsync(); + Task AddAsync(FileSourceInfo fileSourceInfo); + Task UpdateAsync(FileSourceInfo fileSourceInfo); + Task DeleteAsync(Guid id); + Task ExistsAsync(Guid id); + Task> FindAsync(Guid sourceId, ContextType sourceType); + } +} \ No newline at end of file diff --git a/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Infrastructure/Contexts/AppContext.cs b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Infrastructure/Contexts/AppContext.cs new file mode 100644 index 000000000..7d9aa35b8 --- /dev/null +++ b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Infrastructure/Contexts/AppContext.cs @@ -0,0 +1,27 @@ +using MiniSpace.Services.MediaFiles.Application; + +namespace MiniSpace.Services.MediaFiles.Infrastructure.Contexts +{ + internal class AppContext : IAppContext + { + public string RequestId { get; } + public IIdentityContext Identity { get; } + + internal AppContext() : this(Guid.NewGuid().ToString("N"), IdentityContext.Empty) + { + } + + internal AppContext(CorrelationContext context) : this(context.CorrelationId, + context.User is null ? IdentityContext.Empty : new IdentityContext(context.User)) + { + } + + internal AppContext(string requestId, IIdentityContext identity) + { + RequestId = requestId; + Identity = identity; + } + + internal static IAppContext Empty => new AppContext(); + } +} diff --git a/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Infrastructure/Contexts/AppContextFactory.cs b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Infrastructure/Contexts/AppContextFactory.cs new file mode 100644 index 000000000..db1770db5 --- /dev/null +++ b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Infrastructure/Contexts/AppContextFactory.cs @@ -0,0 +1,35 @@ +using Convey.MessageBrokers; +using Microsoft.AspNetCore.Http; +using Newtonsoft.Json; +using MiniSpace.Services.MediaFiles.Application; + +namespace MiniSpace.Services.MediaFiles.Infrastructure.Contexts +{ + internal sealed class AppContextFactory : IAppContextFactory + { + private readonly ICorrelationContextAccessor _contextAccessor; + private readonly IHttpContextAccessor _httpContextAccessor; + + public AppContextFactory(ICorrelationContextAccessor contextAccessor, IHttpContextAccessor httpContextAccessor) + { + _contextAccessor = contextAccessor; + _httpContextAccessor = httpContextAccessor; + } + + public IAppContext Create() + { + if (_contextAccessor.CorrelationContext is { }) + { + var payload = JsonConvert.SerializeObject(_contextAccessor.CorrelationContext); + + return string.IsNullOrWhiteSpace(payload) + ? AppContext.Empty + : new AppContext(JsonConvert.DeserializeObject(payload)); + } + + var context = _httpContextAccessor.GetCorrelationContext(); + + return context is null ? AppContext.Empty : new AppContext(context); + } + } +} diff --git a/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Infrastructure/Contexts/CorrelationContext.cs b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Infrastructure/Contexts/CorrelationContext.cs new file mode 100644 index 000000000..1ba6bbba4 --- /dev/null +++ b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Infrastructure/Contexts/CorrelationContext.cs @@ -0,0 +1,22 @@ +namespace MiniSpace.Services.MediaFiles.Infrastructure.Contexts +{ + internal class CorrelationContext + { + public string CorrelationId { get; set; } + public string SpanContext { get; set; } + public UserContext User { get; set; } + public string ResourceId { get; set; } + public string TraceId { get; set; } + public string ConnectionId { get; set; } + public string Name { get; set; } + public DateTime CreatedAt { get; set; } + + public class UserContext + { + public string Id { get; set; } + public bool IsAuthenticated { get; set; } + public string Role { get; set; } + public IDictionary Claims { get; set; } + } + } +} diff --git a/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Infrastructure/Contexts/IdentityContext.cs b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Infrastructure/Contexts/IdentityContext.cs new file mode 100644 index 000000000..10515178b --- /dev/null +++ b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Infrastructure/Contexts/IdentityContext.cs @@ -0,0 +1,41 @@ +using MiniSpace.Services.MediaFiles.Application; + +namespace MiniSpace.Services.MediaFiles.Infrastructure.Contexts +{ + internal class IdentityContext : IIdentityContext + { + public Guid Id { get; } + public string Role { get; } = string.Empty; + public string Name { get; } = string.Empty; + public string Email { get; } = string.Empty; + public bool IsAuthenticated { get; } + public bool IsAdmin { get; } + public bool IsBanned { get; } + public bool IsOrganizer { get; } + public IDictionary Claims { get; } = new Dictionary(); + + internal IdentityContext() + { + } + + internal IdentityContext(CorrelationContext.UserContext context) + : this(context.Id, context.Role, context.IsAuthenticated, context.Claims) + { + } + + internal IdentityContext(string id, string role, bool isAuthenticated, IDictionary claims) + { + Id = Guid.TryParse(id, out var userId) ? userId : Guid.Empty; + Role = role ?? string.Empty; + IsAuthenticated = isAuthenticated; + IsAdmin = Role.Equals("admin", StringComparison.InvariantCultureIgnoreCase); + IsBanned = Role.Equals("banned", StringComparison.InvariantCultureIgnoreCase); + IsOrganizer = Role.Equals("organizer", StringComparison.InvariantCultureIgnoreCase); + Claims = claims ?? new Dictionary(); + Name = Claims.TryGetValue("name", out var name) ? name : string.Empty; + Email = Claims.TryGetValue("email", out var email) ? email : string.Empty; + } + + internal static IIdentityContext Empty => new IdentityContext(); + } +} diff --git a/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Infrastructure/Decorators/OutboxCommandHandlerDecorator.cs b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Infrastructure/Decorators/OutboxCommandHandlerDecorator.cs new file mode 100644 index 000000000..cce8e588e --- /dev/null +++ b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Infrastructure/Decorators/OutboxCommandHandlerDecorator.cs @@ -0,0 +1,35 @@ +using Convey.CQRS.Commands; +using Convey.MessageBrokers; +using Convey.MessageBrokers.Outbox; +using Convey.Types; + +namespace MiniSpace.Services.MediaFiles.Infrastructure.Decorators +{ + [Decorator] + internal sealed class OutboxCommandHandlerDecorator : ICommandHandler + where TCommand : class, ICommand + { + private readonly ICommandHandler _handler; + private readonly IMessageOutbox _outbox; + private readonly string _messageId; + private readonly bool _enabled; + + public OutboxCommandHandlerDecorator(ICommandHandler handler, IMessageOutbox outbox, + OutboxOptions outboxOptions, IMessagePropertiesAccessor messagePropertiesAccessor) + { + _handler = handler; + _outbox = outbox; + _enabled = outboxOptions.Enabled; + + var messageProperties = messagePropertiesAccessor.MessageProperties; + _messageId = string.IsNullOrWhiteSpace(messageProperties?.MessageId) + ? Guid.NewGuid().ToString("N") + : messageProperties.MessageId; + } + + public Task HandleAsync(TCommand command, CancellationToken cancellationToken) + => _enabled + ? _outbox.HandleAsync(_messageId, () => _handler.HandleAsync(command)) + : _handler.HandleAsync(command); + } +} diff --git a/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Infrastructure/Decorators/OutboxEventHandlerDecorator.cs b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Infrastructure/Decorators/OutboxEventHandlerDecorator.cs new file mode 100644 index 000000000..2fe4847f6 --- /dev/null +++ b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Infrastructure/Decorators/OutboxEventHandlerDecorator.cs @@ -0,0 +1,35 @@ +using Convey.CQRS.Events; +using Convey.MessageBrokers; +using Convey.MessageBrokers.Outbox; +using Convey.Types; + +namespace MiniSpace.Services.MediaFiles.Infrastructure.Decorators +{ + [Decorator] + internal sealed class OutboxEventHandlerDecorator : IEventHandler + where TEvent : class, IEvent + { + private readonly IEventHandler _handler; + private readonly IMessageOutbox _outbox; + private readonly string _messageId; + private readonly bool _enabled; + + public OutboxEventHandlerDecorator(IEventHandler handler, IMessageOutbox outbox, + OutboxOptions outboxOptions, IMessagePropertiesAccessor messagePropertiesAccessor) + { + _handler = handler; + _outbox = outbox; + _enabled = outboxOptions.Enabled; + + var messageProperties = messagePropertiesAccessor.MessageProperties; + _messageId = string.IsNullOrWhiteSpace(messageProperties?.MessageId) + ? Guid.NewGuid().ToString("N") + : messageProperties.MessageId; + } + + public Task HandleAsync(TEvent @event, CancellationToken cancellationToken) + => _enabled + ? _outbox.HandleAsync(_messageId, () => _handler.HandleAsync(@event)) + : _handler.HandleAsync(@event); + } +} diff --git a/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Infrastructure/Exceptions/ExceptionToMessageMapper.cs b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Infrastructure/Exceptions/ExceptionToMessageMapper.cs new file mode 100644 index 000000000..da84e4638 --- /dev/null +++ b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Infrastructure/Exceptions/ExceptionToMessageMapper.cs @@ -0,0 +1,14 @@ +using Convey.MessageBrokers.RabbitMQ; + +namespace MiniSpace.Services.MediaFiles.Infrastructure.Exceptions +{ + internal sealed class ExceptionToMessageMapper : IExceptionToMessageMapper + { + public object Map(Exception exception, object message) + => exception switch + + { + _ => null + }; + } +} diff --git a/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Infrastructure/Exceptions/ExceptionToResponseMapper.cs b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Infrastructure/Exceptions/ExceptionToResponseMapper.cs new file mode 100644 index 000000000..48a0dab98 --- /dev/null +++ b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Infrastructure/Exceptions/ExceptionToResponseMapper.cs @@ -0,0 +1,46 @@ +using System.Collections.Concurrent; +using System.Net; +using Convey; +using Convey.WebApi.Exceptions; +using MiniSpace.Services.MediaFiles.Application.Exceptions; +using MiniSpace.Services.MediaFiles.Core.Exceptions; + +namespace MiniSpace.Services.MediaFiles.Infrastructure.Exceptions +{ + internal sealed class ExceptionToResponseMapper : IExceptionToResponseMapper + { + private static readonly ConcurrentDictionary Codes = new ConcurrentDictionary(); + + public ExceptionResponse Map(Exception exception) + => exception switch + { + DomainException ex => new ExceptionResponse(new {code = GetCode(ex), reason = ex.Message}, + HttpStatusCode.BadRequest), + AppException ex => new ExceptionResponse(new {code = GetCode(ex), reason = ex.Message}, + HttpStatusCode.BadRequest), + _ => new ExceptionResponse(new {code = "error", reason = "There was an error."}, + HttpStatusCode.BadRequest) + }; + + private static string GetCode(Exception exception) + { + var type = exception.GetType(); + if (Codes.TryGetValue(type, out var code)) + { + return code; + } + + var exceptionCode = exception switch + { + DomainException domainException when !string.IsNullOrWhiteSpace(domainException.Code) => domainException + .Code, + AppException appException when !string.IsNullOrWhiteSpace(appException.Code) => appException.Code, + _ => exception.GetType().Name.Underscore().Replace("_exception", string.Empty) + }; + + Codes.TryAdd(type, exceptionCode); + + return exceptionCode; + } + } +} diff --git a/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Infrastructure/Extensions.cs b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Infrastructure/Extensions.cs new file mode 100644 index 000000000..0cec9be95 --- /dev/null +++ b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Infrastructure/Extensions.cs @@ -0,0 +1,147 @@ +using System.Text; +using Convey; +using Convey.CQRS.Commands; +using Convey.CQRS.Events; +using Convey.CQRS.Queries; +using Convey.Discovery.Consul; +using Convey.Docs.Swagger; +using Convey.HTTP; +using Convey.LoadBalancing.Fabio; +using Convey.MessageBrokers; +using Convey.MessageBrokers.CQRS; +using Convey.MessageBrokers.Outbox; +using Convey.MessageBrokers.Outbox.Mongo; +using Convey.MessageBrokers.RabbitMQ; +using Convey.Metrics.AppMetrics; +using Convey.Persistence.MongoDB; +using Convey.Persistence.Redis; +using Convey.Security; +using Convey.Tracing.Jaeger; +using Convey.Tracing.Jaeger.RabbitMQ; +using Convey.WebApi; +using Convey.WebApi.CQRS; +using Convey.WebApi.Security; +using Convey.WebApi.Swagger; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; +using Newtonsoft.Json; +using MiniSpace.Services.MediaFiles.Application; +using MiniSpace.Services.MediaFiles.Application.Commands; +using MiniSpace.Services.MediaFiles.Application.Events.External; +using MiniSpace.Services.MediaFiles.Application.Services; +using MiniSpace.Services.MediaFiles.Core.Repositories; +using MiniSpace.Services.MediaFiles.Infrastructure.Contexts; +using MiniSpace.Services.MediaFiles.Infrastructure.Decorators; +using MiniSpace.Services.MediaFiles.Infrastructure.Exceptions; +using MiniSpace.Services.MediaFiles.Infrastructure.Logging; +using MiniSpace.Services.MediaFiles.Infrastructure.Mongo.Documents; +using MiniSpace.Services.MediaFiles.Infrastructure.Mongo.Repositories; +using MiniSpace.Services.MediaFiles.Infrastructure.Services; +using MiniSpace.Services.MediaFiles.Infrastructure.Services.Workers; +using MongoDB.Driver; + +namespace MiniSpace.Services.MediaFiles.Infrastructure +{ + public static class Extensions + { + public static IConveyBuilder AddInfrastructure(this IConveyBuilder builder) + { + builder.Services.AddTransient(); + builder.Services.AddSingleton(); + builder.Services.AddSingleton(); + builder.Services.AddSingleton(); + builder.Services.AddTransient(); + builder.Services.AddTransient(); + builder.Services.AddTransient(); + builder.Services.AddTransient(ctx => ctx.GetRequiredService().Create()); + builder.Services.TryDecorate(typeof(ICommandHandler<>), typeof(OutboxCommandHandlerDecorator<>)); + builder.Services.TryDecorate(typeof(IEventHandler<>), typeof(OutboxEventHandlerDecorator<>)); + builder.Services.AddSingleton(serviceProvider => + { + var mongoDbOptions = serviceProvider.GetRequiredService(); + var mongoClient = new MongoClient(mongoDbOptions.ConnectionString); + var database = mongoClient.GetDatabase(mongoDbOptions.Database); + return new GridFSService(database); + }); + builder.Services.AddHostedService(); + + return builder + .AddErrorHandler() + .AddQueryHandlers() + .AddInMemoryQueryDispatcher() + .AddHttpClient() + .AddConsul() + .AddFabio() + .AddRabbitMq(plugins: p => p.AddJaegerRabbitMqPlugin()) + .AddMessageOutbox(o => o.AddMongo()) + .AddExceptionToMessageMapper() + .AddMongo() + .AddRedis() + .AddMetrics() + .AddJaeger() + .AddHandlersLogging() + .AddMongoRepository("fileSourceInfos") + .AddWebApiSwaggerDocs() + .AddCertificateAuthentication() + .AddSecurity(); + } + + public static IApplicationBuilder UseInfrastructure(this IApplicationBuilder app) + { + app.UseErrorHandler() + .UseSwaggerDocs() + .UseJaeger() + .UseConvey() + .UsePublicContracts() + .UseMetrics() + .UseCertificateAuthentication() + .UseRabbitMq() + .SubscribeCommand() + .SubscribeCommand() + .SubscribeCommand() + .SubscribeEvent() + .SubscribeEvent() + .SubscribeEvent() + .SubscribeEvent(); + + return app; + } + + internal static CorrelationContext GetCorrelationContext(this IHttpContextAccessor accessor) + => accessor.HttpContext?.Request.Headers.TryGetValue("Correlation-Context", out var json) is true + ? JsonConvert.DeserializeObject(json.FirstOrDefault()) + : null; + + internal static IDictionary GetHeadersToForward(this IMessageProperties messageProperties) + { + const string sagaHeader = "Saga"; + if (messageProperties?.Headers is null || !messageProperties.Headers.TryGetValue(sagaHeader, out var saga)) + { + return null; + } + + return saga is null + ? null + : new Dictionary + { + [sagaHeader] = saga + }; + } + + internal static string GetSpanContext(this IMessageProperties messageProperties, string header) + { + if (messageProperties is null) + { + return string.Empty; + } + + if (messageProperties.Headers.TryGetValue(header, out var span) && span is byte[] spanBytes) + { + return Encoding.UTF8.GetString(spanBytes); + } + + return string.Empty; + } + } +} diff --git a/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Infrastructure/IAppContextFactory.cs b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Infrastructure/IAppContextFactory.cs new file mode 100644 index 000000000..f6a451f19 --- /dev/null +++ b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Infrastructure/IAppContextFactory.cs @@ -0,0 +1,9 @@ +using MiniSpace.Services.MediaFiles.Application; + +namespace MiniSpace.Services.MediaFiles.Infrastructure +{ + public interface IAppContextFactory + { + IAppContext Create(); + } +} diff --git a/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Infrastructure/Logging/Extensions.cs b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Infrastructure/Logging/Extensions.cs new file mode 100644 index 000000000..e358df03b --- /dev/null +++ b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Infrastructure/Logging/Extensions.cs @@ -0,0 +1,21 @@ +using Convey; +using Convey.Logging.CQRS; +using Microsoft.Extensions.DependencyInjection; +using MiniSpace.Services.MediaFiles.Application.Commands; + +namespace MiniSpace.Services.MediaFiles.Infrastructure.Logging +{ + internal static class Extensions + { + public static IConveyBuilder AddHandlersLogging(this IConveyBuilder builder) + { + var assembly = typeof(UploadMediaFile).Assembly; + + builder.Services.AddSingleton(new MessageToLogTemplateMapper()); + + return builder + .AddCommandHandlersLogging(assembly) + .AddEventHandlersLogging(assembly); + } + } +} diff --git a/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Infrastructure/Logging/MessageToLogTemplateMapper.cs b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Infrastructure/Logging/MessageToLogTemplateMapper.cs new file mode 100644 index 000000000..706960785 --- /dev/null +++ b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Infrastructure/Logging/MessageToLogTemplateMapper.cs @@ -0,0 +1,102 @@ +using Convey.Logging.CQRS; +using MiniSpace.Services.MediaFiles.Application.Commands; +using MiniSpace.Services.MediaFiles.Application.Events.External; + +namespace MiniSpace.Services.MediaFiles.Infrastructure.Logging +{ + internal sealed class MessageToLogTemplateMapper : IMessageToLogTemplateMapper + { + private static IReadOnlyDictionary MessageTemplates + => new Dictionary + { + { + typeof(UploadMediaFile), new HandlerLogTemplate + { + After = "Uploaded media file with ID: {MediaFileId} and name: {FileName}.", + } + }, + { + typeof(DeleteMediaFile), new HandlerLogTemplate + { + After = "Deleted media file with ID: {MediaFileId}.", + } + }, + { + typeof(StudentCreated), + new HandlerLogTemplate + { + After = "Associated profile picture with ID: {MediaFileId} for student with ID: {StudentId}.", + } + }, + { + typeof(StudentUpdated), + new HandlerLogTemplate + { + After = "Associated profile picture with ID: {MediaFileId} for student with ID: {StudentId}.", + } + }, + { + typeof(StudentDeleted), + new HandlerLogTemplate + { + After = "Deleted all media files for student with ID: {StudentId}.", + } + }, + { + typeof(PostCreated), + new HandlerLogTemplate + { + After = "Associated media files for post with ID: {PostId}.", + } + }, + { + typeof(PostUpdated), + new HandlerLogTemplate + { + After = "Associated media files for post with ID: {PostId}.", + } + }, + { + typeof(PostDeleted), + new HandlerLogTemplate + { + After = "Deleted all media files for post with ID: {PostId}.", + } + }, + { + typeof(EventCreated), + new HandlerLogTemplate + { + After = "Associated media files for event with ID: {EventId}.", + } + }, + { + typeof(EventUpdated), + new HandlerLogTemplate + { + After = "Associated media files for event with ID: {EventId}.", + } + }, + { + typeof(EventDeleted), + new HandlerLogTemplate + { + After = "Deleted all media files for event with ID: {EventId}.", + } + }, + { + typeof(CleanupUnassociatedFiles), + new HandlerLogTemplate + { + After = "Cleaned unmatched files for all entities at {Now}." + } + }, + }; + + public HandlerLogTemplate Map(TMessage message) where TMessage : class + { + var key = message.GetType(); + return MessageTemplates.TryGetValue(key, out var template) ? template : null; + } + } +} diff --git a/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Infrastructure/MiniSpace.Services.MediaFiles.Infrastructure.csproj b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Infrastructure/MiniSpace.Services.MediaFiles.Infrastructure.csproj new file mode 100644 index 000000000..cbe16ffa6 --- /dev/null +++ b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Infrastructure/MiniSpace.Services.MediaFiles.Infrastructure.csproj @@ -0,0 +1,40 @@ + + + + net8.0 + enable + disable + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Infrastructure/Mongo/Documents/Extensions.cs b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Infrastructure/Mongo/Documents/Extensions.cs new file mode 100644 index 000000000..ca41d0535 --- /dev/null +++ b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Infrastructure/Mongo/Documents/Extensions.cs @@ -0,0 +1,27 @@ +using MiniSpace.Services.MediaFiles.Core.Entities; + +namespace MiniSpace.Services.MediaFiles.Infrastructure.Mongo.Documents +{ + public static class Extensions + { + public static FileSourceInfoDocument AsDocument(this FileSourceInfo fileSourceInfo) + => new FileSourceInfoDocument + { + Id = fileSourceInfo.Id, + SourceId = fileSourceInfo.SourceId, + SourceType = fileSourceInfo.SourceType, + UploaderId = fileSourceInfo.UploaderId, + State = fileSourceInfo.State, + CreatedAt = fileSourceInfo.CreatedAt, + OriginalFileId = fileSourceInfo.OriginalFileId, + OriginalFileContentType = fileSourceInfo.OriginalFileContentType, + FileId = fileSourceInfo.FileId, + FileName = fileSourceInfo.FileName + }; + + public static FileSourceInfo AsEntity(this FileSourceInfoDocument document) + => new FileSourceInfo(document.Id, document.SourceId, document.SourceType, document.UploaderId, + document.State, document.CreatedAt, document.OriginalFileId, document.OriginalFileContentType, + document.FileId, document.FileName); + } +} \ No newline at end of file diff --git a/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Infrastructure/Mongo/Documents/FileSourceInfoDocument.cs b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Infrastructure/Mongo/Documents/FileSourceInfoDocument.cs new file mode 100644 index 000000000..8aba3c17a --- /dev/null +++ b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Infrastructure/Mongo/Documents/FileSourceInfoDocument.cs @@ -0,0 +1,20 @@ +using Convey.Types; +using MiniSpace.Services.MediaFiles.Core.Entities; +using MongoDB.Bson; + +namespace MiniSpace.Services.MediaFiles.Infrastructure.Mongo.Documents +{ + public class FileSourceInfoDocument : IIdentifiable + { + public Guid Id { get; set; } + public Guid SourceId { get; set; } + public ContextType SourceType { get; set; } + public Guid UploaderId { get; set; } + public State State { get; set; } + public DateTime CreatedAt { get; set; } + public ObjectId OriginalFileId { get; set; } + public string OriginalFileContentType { get; set; } + public ObjectId FileId { get; set; } + public string FileName { get; set; } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Infrastructure/Mongo/Queries/Handlers/GetMediaFileHandler.cs b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Infrastructure/Mongo/Queries/Handlers/GetMediaFileHandler.cs new file mode 100644 index 000000000..2d5abd75a --- /dev/null +++ b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Infrastructure/Mongo/Queries/Handlers/GetMediaFileHandler.cs @@ -0,0 +1,45 @@ +using Convey.CQRS.Queries; +using Convey.Persistence.MongoDB; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Net.Http.Headers; +using MiniSpace.Services.MediaFiles.Application.Dto; +using MiniSpace.Services.MediaFiles.Application.Queries; +using MiniSpace.Services.MediaFiles.Application.Services; +using MiniSpace.Services.MediaFiles.Infrastructure.Mongo.Documents; +using MongoDB.Bson; + +namespace MiniSpace.Services.MediaFiles.Infrastructure.Mongo.Queries.Handlers +{ + public class GetMediaFileHandler : IQueryHandler + { + private readonly IMongoRepository _fileSourceInfoRepository; + private readonly IGridFSService _gridFSService; + private const string FileContentType = "image/webp"; + + public GetMediaFileHandler(IMongoRepository fileSourceInfoRepository, + IGridFSService gridFSService) + { + _fileSourceInfoRepository = fileSourceInfoRepository; + _gridFSService = gridFSService; + } + + public async Task HandleAsync(GetMediaFile query, CancellationToken cancellationToken) + { + var fileSourceInfo = await _fileSourceInfoRepository.GetAsync(query.MediaFileId); + if (fileSourceInfo is null) + { + return null; + } + + var fileStream = new MemoryStream(); + await _gridFSService.DownloadFileAsync(fileSourceInfo.FileId, fileStream); + fileStream.Seek(0, SeekOrigin.Begin); + byte[] fileContent = fileStream.ToArray(); + var base64String = Convert.ToBase64String(fileContent); + + return new FileDto(query.MediaFileId, fileSourceInfo.SourceId, fileSourceInfo.SourceType.ToString(), + fileSourceInfo.UploaderId, fileSourceInfo.State.ToString().ToLower(), fileSourceInfo.CreatedAt, + fileSourceInfo.FileName, FileContentType, base64String); + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Infrastructure/Mongo/Queries/Handlers/GetOriginalMediaFileHandler.cs b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Infrastructure/Mongo/Queries/Handlers/GetOriginalMediaFileHandler.cs new file mode 100644 index 000000000..3e8edf827 --- /dev/null +++ b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Infrastructure/Mongo/Queries/Handlers/GetOriginalMediaFileHandler.cs @@ -0,0 +1,44 @@ +using Convey.CQRS.Queries; +using Convey.Persistence.MongoDB; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Net.Http.Headers; +using MiniSpace.Services.MediaFiles.Application.Dto; +using MiniSpace.Services.MediaFiles.Application.Queries; +using MiniSpace.Services.MediaFiles.Application.Services; +using MiniSpace.Services.MediaFiles.Infrastructure.Mongo.Documents; +using MongoDB.Bson; + +namespace MiniSpace.Services.MediaFiles.Infrastructure.Mongo.Queries.Handlers +{ + public class GetOriginalMediaFileHandler : IQueryHandler + { + private readonly IMongoRepository _fileSourceInfoRepository; + private readonly IGridFSService _gridFSService; + + public GetOriginalMediaFileHandler(IMongoRepository fileSourceInfoRepository, + IGridFSService gridFSService) + { + _fileSourceInfoRepository = fileSourceInfoRepository; + _gridFSService = gridFSService; + } + + public async Task HandleAsync(GetOriginalMediaFile query, CancellationToken cancellationToken) + { + var fileSourceInfo = await _fileSourceInfoRepository.GetAsync(query.MediaFileId); + if (fileSourceInfo is null) + { + return null; + } + + var fileStream = new MemoryStream(); + await _gridFSService.DownloadFileAsync(fileSourceInfo.OriginalFileId, fileStream); + fileStream.Seek(0, SeekOrigin.Begin); + byte[] fileContent = fileStream.ToArray(); + var base64String = Convert.ToBase64String(fileContent); + + return new FileDto(query.MediaFileId, fileSourceInfo.SourceId, fileSourceInfo.SourceType.ToString(), + fileSourceInfo.UploaderId, fileSourceInfo.State.ToString().ToLower(), fileSourceInfo.CreatedAt, + fileSourceInfo.FileName, fileSourceInfo.OriginalFileContentType, base64String); + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Infrastructure/Mongo/Repositories/FileSourceInfoMongoRepository.cs b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Infrastructure/Mongo/Repositories/FileSourceInfoMongoRepository.cs new file mode 100644 index 000000000..c3e80cbc3 --- /dev/null +++ b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Infrastructure/Mongo/Repositories/FileSourceInfoMongoRepository.cs @@ -0,0 +1,58 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Convey.Persistence.MongoDB; +using MiniSpace.Services.MediaFiles.Core.Entities; +using MiniSpace.Services.MediaFiles.Core.Repositories; +using MiniSpace.Services.MediaFiles.Infrastructure.Mongo.Documents; +using MongoDB.Driver; +using MongoDB.Driver.Linq; + +namespace MiniSpace.Services.MediaFiles.Infrastructure.Mongo.Repositories +{ + public class FileSourceInfoMongoRepository : IFileSourceInfoRepository + { + private readonly IMongoRepository _repository; + + public FileSourceInfoMongoRepository(IMongoRepository repository) + { + _repository = repository; + } + + public async Task GetAsync(Guid id) + { + var fileSourceInfo = await _repository.GetAsync(s => s.Id == id); + + return fileSourceInfo?.AsEntity(); + } + + public async Task> GetAllUnassociatedAsync() + { + var fileSourceInfos = await _repository.FindAsync(s => s.State == State.Unassociated); + + return fileSourceInfos?.Select(s => s.AsEntity()); + } + + public Task AddAsync(FileSourceInfo fileSourceInfo) + => _repository.AddAsync(fileSourceInfo.AsDocument()); + + public Task UpdateAsync(FileSourceInfo fileSourceInfo) + => _repository.UpdateAsync(fileSourceInfo.AsDocument()); + + public Task DeleteAsync(Guid id) + => _repository.DeleteAsync(id); + + public Task ExistsAsync(Guid id) + => _repository.ExistsAsync(s => s.Id == id); + + public async Task> FindAsync(Guid sourceId, ContextType sourceType) + { + var fileSourceInfos = await _repository.FindAsync( + s => s.SourceId == sourceId && s.SourceType == sourceType); + + return fileSourceInfos?.Select(s => s.AsEntity()); + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Infrastructure/Services/DateTimeProvider.cs b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Infrastructure/Services/DateTimeProvider.cs new file mode 100644 index 000000000..539d6089b --- /dev/null +++ b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Infrastructure/Services/DateTimeProvider.cs @@ -0,0 +1,9 @@ +using MiniSpace.Services.MediaFiles.Application.Services; + +namespace MiniSpace.Services.MediaFiles.Infrastructure.Services +{ + internal sealed class DateTimeProvider : IDateTimeProvider + { + public DateTime Now => DateTime.UtcNow; + } +} diff --git a/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Infrastructure/Services/EventMapper.cs b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Infrastructure/Services/EventMapper.cs new file mode 100644 index 000000000..ff2fb0c3f --- /dev/null +++ b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Infrastructure/Services/EventMapper.cs @@ -0,0 +1,23 @@ +using Convey.CQRS.Events; +using MiniSpace.Services.MediaFiles.Application.Services; +using MiniSpace.Services.MediaFiles.Core; +using MiniSpace.Services.MediaFiles.Core.Events; + +namespace MiniSpace.Services.MediaFiles.Infrastructure.Services +{ + public class EventMapper : IEventMapper + { + public IEnumerable MapAll(IEnumerable events) + => events.Select(Map); + + public IEvent Map(IDomainEvent @event) + { + switch (@event) + { + + } + + return null; + } + } +} diff --git a/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Infrastructure/Services/FileValidator.cs b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Infrastructure/Services/FileValidator.cs new file mode 100644 index 000000000..30cf2b34d --- /dev/null +++ b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Infrastructure/Services/FileValidator.cs @@ -0,0 +1,49 @@ +using MiniSpace.Services.MediaFiles.Application.Exceptions; +using MiniSpace.Services.MediaFiles.Application.Services; + +namespace MiniSpace.Services.MediaFiles.Infrastructure.Services +{ + public class FileValidator : IFileValidator + { + private const int MaxFileSize = 1_000_000; + + private readonly Dictionary _mimeTypes = new Dictionary() + { + { "FFD8FFDB", "image/jpeg" }, + { "FFD8FFE0", "image/jpeg" }, + { "FFD8FFE1", "image/jpeg" }, + { "FFD8FFE2", "image/jpeg" }, + { "FFD8FFEE", "image/jpeg" }, + { "89504E47", "image/png" }, + { "47494638", "image/gif" }, + { "49492A00", "image/tiff" }, + { "4D4D002A", "image/tiff" }, + { "52494646", "image/webp" }, + { "57454250", "image/webp" } + }; + + public void ValidateFileSize(int size) + { + if (size > MaxFileSize) + { + throw new InvalidFileSizeException(size, MaxFileSize); + } + } + + public void ValidateFileExtensions(byte[] bytes, string contentType) + { + if (!_mimeTypes.ContainsValue(contentType)) + { + throw new InvalidFileContentTypeException(contentType); + } + + string hex = BitConverter.ToString(bytes, 0, 4).Replace("-", string.Empty); + _mimeTypes.TryGetValue(hex, out var mimeType); + if (mimeType != contentType) + { + throw new FileTypeDoesNotMatchContentTypeException(mimeType, contentType); + } + } + + } +} \ No newline at end of file diff --git a/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Infrastructure/Services/GridFSService.cs b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Infrastructure/Services/GridFSService.cs new file mode 100644 index 000000000..35c7e60a4 --- /dev/null +++ b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Infrastructure/Services/GridFSService.cs @@ -0,0 +1,35 @@ +using MiniSpace.Services.MediaFiles.Application.Services; +using MongoDB.Bson; +using MongoDB.Driver; +using MongoDB.Driver.GridFS; + +namespace MiniSpace.Services.MediaFiles.Infrastructure.Services +{ + public class GridFSService: IGridFSService + { + private readonly IMongoDatabase _database; + private readonly GridFSBucket _gridFSBucket; + + public GridFSService(IMongoDatabase database) + { + _database = database; + _gridFSBucket = new GridFSBucket(_database); + } + + public async Task UploadFileAsync(string fileName, Stream fileStream) + { + ObjectId fileId = await _gridFSBucket.UploadFromStreamAsync(fileName, fileStream); + return fileId; + } + + public async Task DownloadFileAsync(ObjectId fileId, Stream destination) + { + await _gridFSBucket.DownloadToStreamAsync(fileId, destination); + } + + public async Task DeleteFileAsync(ObjectId fileId) + { + await _gridFSBucket.DeleteAsync(fileId); + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Infrastructure/Services/MediaFilesService.cs b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Infrastructure/Services/MediaFilesService.cs new file mode 100644 index 000000000..4d540f282 --- /dev/null +++ b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Infrastructure/Services/MediaFilesService.cs @@ -0,0 +1,71 @@ +using MiniSpace.Services.MediaFiles.Application; +using MiniSpace.Services.MediaFiles.Application.Commands; +using MiniSpace.Services.MediaFiles.Application.Dto; +using MiniSpace.Services.MediaFiles.Application.Events; +using MiniSpace.Services.MediaFiles.Application.Exceptions; +using MiniSpace.Services.MediaFiles.Application.Services; +using MiniSpace.Services.MediaFiles.Core.Entities; +using MiniSpace.Services.MediaFiles.Core.Repositories; +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Formats.Webp; + +namespace MiniSpace.Services.MediaFiles.Infrastructure.Services +{ + public class MediaFilesService: IMediaFilesService + { + private readonly IFileSourceInfoRepository _fileSourceInfoRepository; + private readonly IFileValidator _fileValidator; + private readonly IGridFSService _gridFSService; + private readonly IDateTimeProvider _dateTimeProvider; + private readonly IAppContext _appContext; + private readonly IMessageBroker _messageBroker; + + public MediaFilesService(IFileSourceInfoRepository fileSourceInfoRepository, IFileValidator fileValidator, + IGridFSService gridFSService, IDateTimeProvider dateTimeProvider, IAppContext appContext, + IMessageBroker messageBroker) + { + _fileSourceInfoRepository = fileSourceInfoRepository; + _fileValidator = fileValidator; + _gridFSService = gridFSService; + _dateTimeProvider = dateTimeProvider; + _appContext = appContext; + _messageBroker = messageBroker; + } + + public async Task UploadAsync(UploadMediaFile command) + { + var identity = _appContext.Identity; + if(identity.IsAuthenticated && identity.Id != command.UploaderId) + { + throw new UnauthorizedMediaFileUploadException(identity.Id, command.UploaderId); + } + + if (!Enum.TryParse(command.SourceType, out ContextType sourceType)) + { + throw new InvalidContextTypeException(command.SourceType); + } + + byte[] bytes = Convert.FromBase64String(command.Base64Content); + _fileValidator.ValidateFileSize(bytes.Length); + _fileValidator.ValidateFileExtensions(bytes, command.FileContentType); + + using var inStream = new MemoryStream(bytes); + using var myImage = await Image.LoadAsync(inStream); + using var outStream = new MemoryStream(); + await myImage.SaveAsync(outStream, new WebpEncoder()); + inStream.Position = 0; + outStream.Position = 0; + + var originalObjectId = await _gridFSService.UploadFileAsync(command.FileName, inStream); + var objectId = await _gridFSService.UploadFileAsync(command.FileName, outStream); + var fileSourceInfo = new FileSourceInfo(command.MediaFileId, command.SourceId, sourceType, + command.UploaderId, State.Unassociated, _dateTimeProvider.Now, originalObjectId, + command.FileContentType, objectId, command.FileName); + await _fileSourceInfoRepository.AddAsync(fileSourceInfo); + await _messageBroker.PublishAsync(new MediaFileUploaded(command.MediaFileId, command.FileName)); + + return new FileUploadResponseDto(fileSourceInfo.Id); + } + + } +} \ No newline at end of file diff --git a/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Infrastructure/Services/MessageBroker.cs b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Infrastructure/Services/MessageBroker.cs new file mode 100644 index 000000000..5c35834ef --- /dev/null +++ b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Infrastructure/Services/MessageBroker.cs @@ -0,0 +1,84 @@ +using Convey.CQRS.Events; +using Convey.MessageBrokers; +using Convey.MessageBrokers.Outbox; +using Convey.MessageBrokers.RabbitMQ; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Logging; +using OpenTracing; +using MiniSpace.Services.MediaFiles.Application.Services; + +namespace MiniSpace.Services.MediaFiles.Infrastructure.Services +{ + internal sealed class MessageBroker : IMessageBroker + { + private const string DefaultSpanContextHeader = "span_context"; + private readonly IBusPublisher _busPublisher; + private readonly IMessageOutbox _outbox; + private readonly ICorrelationContextAccessor _contextAccessor; + private readonly IHttpContextAccessor _httpContextAccessor; + private readonly IMessagePropertiesAccessor _messagePropertiesAccessor; + private readonly ITracer _tracer; + private readonly ILogger _logger; + private readonly string _spanContextHeader; + + public MessageBroker(IBusPublisher busPublisher, IMessageOutbox outbox, + ICorrelationContextAccessor contextAccessor, IHttpContextAccessor httpContextAccessor, + IMessagePropertiesAccessor messagePropertiesAccessor, RabbitMqOptions options, ITracer tracer, + ILogger logger) + { + _busPublisher = busPublisher; + _outbox = outbox; + _contextAccessor = contextAccessor; + _httpContextAccessor = httpContextAccessor; + _messagePropertiesAccessor = messagePropertiesAccessor; + _tracer = tracer; + _logger = logger; + _spanContextHeader = string.IsNullOrWhiteSpace(options.SpanContextHeader) + ? DefaultSpanContextHeader + : options.SpanContextHeader; + } + + public Task PublishAsync(params IEvent[] events) => PublishAsync(events?.AsEnumerable()); + + public async Task PublishAsync(IEnumerable events) + { + if (events is null) + { + return; + } + + var messageProperties = _messagePropertiesAccessor.MessageProperties; + var originatedMessageId = messageProperties?.MessageId; + var correlationId = messageProperties?.CorrelationId; + var spanContext = messageProperties?.GetSpanContext(_spanContextHeader); + if (string.IsNullOrWhiteSpace(spanContext)) + { + spanContext = _tracer.ActiveSpan is null ? string.Empty : _tracer.ActiveSpan.Context.ToString(); + } + + var headers = messageProperties.GetHeadersToForward(); + var correlationContext = _contextAccessor.CorrelationContext ?? + _httpContextAccessor.GetCorrelationContext(); + + foreach (var @event in events) + { + if (@event is null) + { + continue; + } + + var messageId = Guid.NewGuid().ToString("N"); + _logger.LogTrace($"Publishing integration event: {@event.GetType().Name} [id: '{messageId}']."); + if (_outbox.Enabled) + { + await _outbox.SendAsync(@event, originatedMessageId, messageId, correlationId, spanContext, + correlationContext, headers); + continue; + } + + await _busPublisher.PublishAsync(@event, messageId, correlationId, spanContext, correlationContext, + headers); + } + } + } +} diff --git a/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Infrastructure/Services/Workers/FileCleanupWorker.cs b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Infrastructure/Services/Workers/FileCleanupWorker.cs new file mode 100644 index 000000000..243d9da1b --- /dev/null +++ b/MiniSpace.Services.MediaFiles/src/MiniSpace.Services.MediaFiles.Infrastructure/Services/Workers/FileCleanupWorker.cs @@ -0,0 +1,57 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using Convey.CQRS.Commands; +using Convey.Persistence.MongoDB; +using Microsoft.Extensions.Hosting; +using MiniSpace.Services.MediaFiles.Application.Commands; +using MiniSpace.Services.MediaFiles.Application.Events; +using MiniSpace.Services.MediaFiles.Application.Services; +using MiniSpace.Services.MediaFiles.Infrastructure.Mongo.Documents; + +namespace MiniSpace.Services.MediaFiles.Infrastructure.Services.Workers +{ + public class FileCleanupWorker: BackgroundService + { + private readonly IMessageBroker _messageBroker; + private readonly ICommandDispatcher _commandDispatcher; + private readonly IDateTimeProvider _dateTimeProvider; + private const int MinutesInterval = 10; + + public FileCleanupWorker(IMessageBroker messageBroker, ICommandDispatcher commandDispatcher, + IDateTimeProvider dateTimeProvider) + { + _messageBroker = messageBroker; + _commandDispatcher = commandDispatcher; + _dateTimeProvider = dateTimeProvider; + } + + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + await _messageBroker.PublishAsync(new FileCleanupBackgroundWorkerStarted(_dateTimeProvider.Now)); + while (!stoppingToken.IsCancellationRequested) + { + try + { + var now = _dateTimeProvider.Now; + var minutes = now.Minute; + if (minutes % MinutesInterval == 0) + { + await _commandDispatcher.SendAsync(new CleanupUnassociatedFiles(now), stoppingToken); + } + + var nextTime = now.AddMinutes(MinutesInterval - (minutes % MinutesInterval)).AddSeconds(-now.Second) + .AddMilliseconds(-now.Millisecond); + var delay = nextTime - now; + + await Task.Delay(delay, stoppingToken); + } + catch (TaskCanceledException) + { + await _messageBroker.PublishAsync(new FileCleanupBackgroundWorkerStopped(_dateTimeProvider.Now)); + return; + } + } + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Commands/CreatePost.cs b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Commands/CreatePost.cs index 0291c545f..fdb630f37 100644 --- a/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Commands/CreatePost.cs +++ b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Commands/CreatePost.cs @@ -8,18 +8,18 @@ public class CreatePost : ICommand public Guid EventId { get; } public Guid OrganizerId { get; } public string TextContent { get; } - public string MediaContent { get; } + public IEnumerable MediaFiles { get; } public string State { get; } public DateTime? PublishDate { get; } public CreatePost(Guid postId, Guid eventId, Guid organizerId, string textContent, - string mediaContent, string state, DateTime? publishDate) + IEnumerable mediaFiles, string state, DateTime? publishDate) { - PostId = postId == Guid.Empty ? Guid.NewGuid() : postId; + PostId = postId; EventId = eventId; OrganizerId = organizerId; TextContent = textContent; - MediaContent = mediaContent; + MediaFiles = mediaFiles; State = state; PublishDate = publishDate; } diff --git a/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Commands/Handlers/CreatePostHandler.cs b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Commands/Handlers/CreatePostHandler.cs index 132d00a84..44811ab9b 100644 --- a/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Commands/Handlers/CreatePostHandler.cs +++ b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Commands/Handlers/CreatePostHandler.cs @@ -39,11 +39,22 @@ public async Task HandleAsync(CreatePost command, CancellationToken cancellation { throw new UnauthorizedPostCreationAttemptException(identity.Id, command.EventId); } + + if(command.PostId == Guid.Empty || await _postRepository.ExistsAsync(command.PostId)) + { + throw new InvalidPostIdException(command.PostId); + } if (!Enum.TryParse(command.State, true, out var newState)) { throw new InvalidPostStateException(command.State); } + + var mediaFiles = command.MediaFiles.ToList(); + if(mediaFiles.Count > 3) + { + throw new InvalidNumberOfPostMediaFilesException(command.PostId, mediaFiles.Count); + } switch (newState) { @@ -54,10 +65,10 @@ public async Task HandleAsync(CreatePost command, CancellationToken cancellation } var post = Post.Create(command.PostId, command.EventId, command.OrganizerId, command.TextContent, - command.MediaContent, _dateTimeProvider.Now, newState, command.PublishDate); + command.MediaFiles, _dateTimeProvider.Now, newState, command.PublishDate); await _postRepository.AddAsync(post); - await _messageBroker.PublishAsync(new PostCreated(command.PostId)); + await _messageBroker.PublishAsync(new PostCreated(command.PostId, post.MediaFiles)); } } } diff --git a/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Commands/Handlers/UpdatePostHandler.cs b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Commands/Handlers/UpdatePostHandler.cs index f14af1fab..ee725ab9e 100644 --- a/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Commands/Handlers/UpdatePostHandler.cs +++ b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Commands/Handlers/UpdatePostHandler.cs @@ -42,10 +42,16 @@ public async Task HandleAsync(UpdatePost command, CancellationToken cancellation throw new UnauthorizedPostOperationException(command.PostId, identity.Id); } - post.Update(command.TextContent, command.MediaContent, _dateTimeProvider.Now); + var mediaFiles = command.MediaFiles.ToList(); + if(mediaFiles.Count > 3) + { + throw new InvalidNumberOfPostMediaFilesException(post.Id, mediaFiles.Count); + } + + post.Update(command.TextContent, command.MediaFiles, _dateTimeProvider.Now); await _postRepository.UpdateAsync(post); - await _messageBroker.PublishAsync(new PostUpdated(command.PostId)); + await _messageBroker.PublishAsync(new PostUpdated(command.PostId, post.MediaFiles)); } } } diff --git a/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Commands/UpdatePost.cs b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Commands/UpdatePost.cs index 00f6d6f40..e180055a3 100644 --- a/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Commands/UpdatePost.cs +++ b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Commands/UpdatePost.cs @@ -6,13 +6,13 @@ public class UpdatePost : ICommand { public Guid PostId { get; } public string TextContent { get; } - public string MediaContent { get; } + public IEnumerable MediaFiles { get; } - public UpdatePost(Guid postId, string textContent, string mediaContent) + public UpdatePost(Guid postId, string textContent, IEnumerable mediaFiles) { PostId = postId; TextContent = textContent; - MediaContent = mediaContent; + MediaFiles = mediaFiles; } } } diff --git a/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Dto/PostDto.cs b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Dto/PostDto.cs index cfce86bad..49c7359cb 100644 --- a/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Dto/PostDto.cs +++ b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Dto/PostDto.cs @@ -6,7 +6,7 @@ public class PostDto public Guid EventId { get; set; } public Guid OrganizerId { get; set; } public string TextContent { get; set; } - public string MediaContent { get; set; } + public IEnumerable MediaFiles { get; set; } public string State { get; set; } public DateTime? PublishDate { get; set; } public DateTime CreatedAt { get; set; } diff --git a/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Events/External/Handlers/MediaFileDeletedHandler.cs b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Events/External/Handlers/MediaFileDeletedHandler.cs new file mode 100644 index 000000000..971421190 --- /dev/null +++ b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Events/External/Handlers/MediaFileDeletedHandler.cs @@ -0,0 +1,33 @@ +using Convey.CQRS.Events; +using MiniSpace.Services.Posts.Application.Services; +using MiniSpace.Services.Posts.Core.Repositories; + +namespace MiniSpace.Services.Posts.Application.Events.External.Handlers +{ + public class MediaFileDeletedHandler: IEventHandler + { + private readonly IPostRepository _postRepository; + private readonly IDateTimeProvider _dateTimeProvider; + + public MediaFileDeletedHandler(IPostRepository postRepository, IDateTimeProvider dateTimeProvider) + { + _postRepository = postRepository; + _dateTimeProvider = dateTimeProvider; + } + + public async Task HandleAsync(MediaFileDeleted @event, CancellationToken cancellationToken) + { + if(@event.Source.ToLowerInvariant() != "post") + { + return; + } + + var post = await _postRepository.GetAsync(@event.SourceId); + if(post != null) + { + post.RemoveMediaFile(@event.MediaFileId, _dateTimeProvider.Now); + await _postRepository.UpdateAsync(post); + } + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Events/External/MediaFileDeleted.cs b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Events/External/MediaFileDeleted.cs new file mode 100644 index 000000000..f79ca8647 --- /dev/null +++ b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Events/External/MediaFileDeleted.cs @@ -0,0 +1,20 @@ +using Convey.CQRS.Events; +using Convey.MessageBrokers; + +namespace MiniSpace.Services.Posts.Application.Events.External +{ + [Message("mediafiles")] + public class MediaFileDeleted: IEvent + { + public Guid MediaFileId { get; } + public Guid SourceId { get; } + public string Source { get; } + + public MediaFileDeleted(Guid mediaFileId, Guid sourceId, string source) + { + MediaFileId = mediaFileId; + SourceId = sourceId; + Source = source; + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Events/PostCreated.cs b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Events/PostCreated.cs index 6675de454..bf44c36ed 100644 --- a/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Events/PostCreated.cs +++ b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Events/PostCreated.cs @@ -5,10 +5,12 @@ namespace MiniSpace.Services.Posts.Application.Events public class PostCreated : IEvent { public Guid PostId { get; } + public IEnumerable MediaFilesIds { get; } - public PostCreated(Guid postId) + public PostCreated(Guid postId, IEnumerable mediaFilesIds) { PostId = postId; + MediaFilesIds = mediaFilesIds; } } } diff --git a/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Events/PostUpdated.cs b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Events/PostUpdated.cs index f6471e54f..e1a6a3d95 100644 --- a/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Events/PostUpdated.cs +++ b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Events/PostUpdated.cs @@ -5,10 +5,12 @@ namespace MiniSpace.Services.Posts.Application.Events public class PostUpdated : IEvent { public Guid PostId { get; } + public IEnumerable MediaFilesIds { get; } - public PostUpdated(Guid postId) + public PostUpdated(Guid postId, IEnumerable mediaFilesIds) { PostId = postId; + MediaFilesIds = mediaFilesIds; } } } diff --git a/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Exceptions/InvalidNumberOfPostMediaFilesException.cs b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Exceptions/InvalidNumberOfPostMediaFilesException.cs new file mode 100644 index 000000000..481fd5d4e --- /dev/null +++ b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Exceptions/InvalidNumberOfPostMediaFilesException.cs @@ -0,0 +1,16 @@ +namespace MiniSpace.Services.Posts.Application.Exceptions +{ + public class InvalidNumberOfPostMediaFilesException : AppException + { + public override string Code { get; } = "invalid_number_of_post_media_files"; + public Guid PostId { get; } + public int MediaSizeNumber { get; } + + public InvalidNumberOfPostMediaFilesException(Guid postId, int mediaFilesNumber) + : base($"Invalid media files number: {mediaFilesNumber} for post with ID: '{postId}'. It should be less or equal 3.") + { + PostId = postId; + MediaSizeNumber = mediaFilesNumber; + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Exceptions/InvalidPostIdException.cs b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Exceptions/InvalidPostIdException.cs new file mode 100644 index 000000000..13b2d64b0 --- /dev/null +++ b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Application/Exceptions/InvalidPostIdException.cs @@ -0,0 +1,12 @@ +namespace MiniSpace.Services.Posts.Application.Exceptions +{ + public class InvalidPostIdException : AppException + { + public Guid PostId { get; } + + public InvalidPostIdException(Guid postId) : base($"Invalid post id: {postId}") + { + PostId = postId; + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Core/Entities/Post.cs b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Core/Entities/Post.cs index d97c788c3..ab2769461 100644 --- a/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Core/Entities/Post.cs +++ b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Core/Entities/Post.cs @@ -7,20 +7,20 @@ public class Post : AggregateRoot public Guid EventId { get; private set; } public Guid OrganizerId { get; private set; } public string TextContent { get; private set; } - public string MediaContent { get; private set; } + public IEnumerable MediaFiles { get; private set; } public State State { get; private set; } public DateTime? PublishDate { get; private set; } public DateTime CreatedAt { get; private set; } public DateTime? UpdatedAt { get; private set; } - public Post(Guid id, Guid eventId, Guid organizerId, string textContent, - string mediaContent, DateTime createdAt, State state, DateTime? publishDate, DateTime? updatedAt = null) + public Post(Guid id, Guid eventId, Guid organizerId, string textContent, IEnumerable mediaFiles, + DateTime createdAt, State state, DateTime? publishDate, DateTime? updatedAt = null) { Id = id; EventId = eventId; OrganizerId = organizerId; TextContent = textContent; - MediaContent = mediaContent; + MediaFiles = mediaFiles; CreatedAt = createdAt; UpdatedAt = updatedAt; State = state; @@ -68,22 +68,31 @@ public bool UpdateState(DateTime now) } public static Post Create(AggregateId id, Guid eventId, Guid studentId, string textContent, - string mediaContent, DateTime createdAt, State state, DateTime? publishDate) + IEnumerable mediaFiles, DateTime createdAt, State state, DateTime? publishDate) { CheckTextContent(id, textContent); - return new Post(id, eventId, studentId, textContent, mediaContent, createdAt, state, publishDate); + return new Post(id, eventId, studentId, textContent, mediaFiles, createdAt, state, publishDate); } - public void Update(string textContent, string mediaContent, DateTime now) + public void Update(string textContent, IEnumerable mediaFiles, DateTime now) { CheckTextContent(Id, textContent); TextContent = textContent; - MediaContent = mediaContent; + MediaFiles = mediaFiles; UpdatedAt = now; } + public void RemoveMediaFile(Guid mediaFileId, DateTime now) + { + var mediaFile = MediaFiles.SingleOrDefault(mf => mf == mediaFileId); + if (mediaFile == Guid.Empty) + { + throw new MediaFileNotFoundException(mediaFileId, Id); + } + } + private static void CheckTextContent(AggregateId id, string textContent) { if (string.IsNullOrWhiteSpace(textContent) || textContent.Length > 5000) diff --git a/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Core/Exceptions/MediaFileNotFoundException.cs b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Core/Exceptions/MediaFileNotFoundException.cs new file mode 100644 index 000000000..4a68fdb63 --- /dev/null +++ b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Core/Exceptions/MediaFileNotFoundException.cs @@ -0,0 +1,16 @@ +namespace MiniSpace.Services.Posts.Core.Exceptions +{ + public class MediaFileNotFoundException : DomainException + { + public override string Code { get; } = "media_file_not_found"; + public Guid MediaFileId { get; } + public Guid PostId { get; } + + public MediaFileNotFoundException(Guid mediaFileId, Guid postId) + : base($"Media file with ID: '{mediaFileId}' for post with ID: {postId} was not found.") + { + MediaFileId = mediaFileId; + PostId = postId; + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Core/Repositories/IPostRepository.cs b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Core/Repositories/IPostRepository.cs index cb9a727d7..56566f803 100644 --- a/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Core/Repositories/IPostRepository.cs +++ b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Core/Repositories/IPostRepository.cs @@ -10,5 +10,6 @@ public interface IPostRepository Task AddAsync(Post post); Task UpdateAsync(Post post); Task DeleteAsync(Guid id); + Task ExistsAsync(Guid id); } } diff --git a/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Infrastructure/Mongo/Documents/Extensions.cs b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Infrastructure/Mongo/Documents/Extensions.cs index 86c7f63da..f02e7606b 100644 --- a/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Infrastructure/Mongo/Documents/Extensions.cs +++ b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Infrastructure/Mongo/Documents/Extensions.cs @@ -7,7 +7,7 @@ public static class Extensions { public static Post AsEntity(this PostDocument document) => new Post(document.Id, document.EventId, document.OrganizerId, document.TextContent, - document.MediaContent, document.CreatedAt, document.State, document.PublishDate, document.UpdatedAt); + document.MediaFiles, document.CreatedAt, document.State, document.PublishDate, document.UpdatedAt); public static PostDocument AsDocument(this Post entity) => new PostDocument() @@ -16,7 +16,7 @@ public static PostDocument AsDocument(this Post entity) EventId = entity.EventId, OrganizerId = entity.OrganizerId, TextContent = entity.TextContent, - MediaContent = entity.MediaContent, + MediaFiles = entity.MediaFiles, CreatedAt = entity.CreatedAt, UpdatedAt = entity.UpdatedAt, State = entity.State, @@ -30,7 +30,7 @@ public static PostDto AsDto(this PostDocument document) EventId = document.EventId, OrganizerId = document.OrganizerId, TextContent = document.TextContent, - MediaContent = document.MediaContent, + MediaFiles = document.MediaFiles, CreatedAt = document.CreatedAt, UpdatedAt = document.UpdatedAt, State = document.State.ToString().ToLowerInvariant(), diff --git a/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Infrastructure/Mongo/Documents/PostDocument.cs b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Infrastructure/Mongo/Documents/PostDocument.cs index 7ca45ee4e..12af21c96 100644 --- a/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Infrastructure/Mongo/Documents/PostDocument.cs +++ b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Infrastructure/Mongo/Documents/PostDocument.cs @@ -9,7 +9,7 @@ public class PostDocument : IIdentifiable public Guid EventId { get; set; } public Guid OrganizerId { get; set; } public string TextContent { get; set; } - public string MediaContent { get; set; } + public IEnumerable MediaFiles { get; set; } public State State { get; set; } public DateTime? PublishDate { get; set; } public DateTime CreatedAt { get; set; } diff --git a/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Infrastructure/Mongo/Repositories/PostMongoRepository.cs b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Infrastructure/Mongo/Repositories/PostMongoRepository.cs index 7d6985b1d..86d5c5ff8 100644 --- a/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Infrastructure/Mongo/Repositories/PostMongoRepository.cs +++ b/MiniSpace.Services.Posts/src/MiniSpace.Services.Posts.Infrastructure/Mongo/Repositories/PostMongoRepository.cs @@ -46,5 +46,8 @@ public Task UpdateAsync(Post post) public Task DeleteAsync(Guid id) => _repository.DeleteAsync(id); + + public Task ExistsAsync(Guid id) + => _repository.ExistsAsync(p => p.Id == id); } } diff --git a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Commands/CompleteStudentRegistration.cs b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Commands/CompleteStudentRegistration.cs index 11713de39..7c159bb66 100644 --- a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Commands/CompleteStudentRegistration.cs +++ b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Commands/CompleteStudentRegistration.cs @@ -5,12 +5,12 @@ namespace MiniSpace.Services.Students.Application.Commands public class CompleteStudentRegistration : ICommand { public Guid StudentId { get; } - public string ProfileImage { get; } + public Guid ProfileImage { get; } public string Description { get; } public DateTime DateOfBirth { get; } public bool EmailNotifications { get; } - public CompleteStudentRegistration(Guid studentId, string profileImage, + public CompleteStudentRegistration(Guid studentId, Guid profileImage, string description, DateTime dateOfBirth, bool emailNotifications) { StudentId = studentId; diff --git a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Commands/UpdateStudent.cs b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Commands/UpdateStudent.cs index d76bb3ba2..ecd0e256f 100644 --- a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Commands/UpdateStudent.cs +++ b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Commands/UpdateStudent.cs @@ -5,11 +5,11 @@ namespace MiniSpace.Services.Students.Application.Commands public class UpdateStudent : ICommand { public Guid StudentId { get; } - public string ProfileImage { get; } + public Guid ProfileImage { get; } public string Description { get; } public bool EmailNotifications { get; } - public UpdateStudent(Guid studentId, string profileImage, string description, bool emailNotifications) + public UpdateStudent(Guid studentId, Guid profileImage, string description, bool emailNotifications) { StudentId = studentId; ProfileImage = profileImage; diff --git a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Dto/StudentDto.cs b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Dto/StudentDto.cs index 6f8d2768b..25dc39f02 100644 --- a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Dto/StudentDto.cs +++ b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Dto/StudentDto.cs @@ -7,7 +7,7 @@ public class StudentDto public string FirstName { get; set; } public string LastName { get; set; } public int NumberOfFriends { get; set; } - public string ProfileImage { get; set; } + public Guid ProfileImage { get; set; } public string Description { get; set; } public DateTime? DateOfBirth { get; set; } public bool EmailNotifications { get; set; } diff --git a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Events/External/Handlers/MediaFileDeletedHandler.cs b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Events/External/Handlers/MediaFileDeletedHandler.cs new file mode 100644 index 000000000..bb3dfc3f4 --- /dev/null +++ b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Events/External/Handlers/MediaFileDeletedHandler.cs @@ -0,0 +1,30 @@ +using Convey.CQRS.Events; +using MiniSpace.Services.Students.Core.Repositories; + +namespace MiniSpace.Services.Students.Application.Events.External.Handlers +{ + public class MediaFileDeletedHandler: IEventHandler + { + private readonly IStudentRepository _studentRepository; + + public MediaFileDeletedHandler(IStudentRepository studentRepository) + { + _studentRepository = studentRepository; + } + + public async Task HandleAsync(MediaFileDeleted @event, CancellationToken cancellationToken) + { + if(@event.Source.ToLowerInvariant() != "studentprofile") + { + return; + } + + var student = await _studentRepository.GetAsync(@event.SourceId); + if(student != null) + { + student.RemoveProfileImage(@event.MediaFileId); + await _studentRepository.UpdateAsync(student); + } + } + } +} diff --git a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Events/External/MediaFileDeleted.cs b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Events/External/MediaFileDeleted.cs new file mode 100644 index 000000000..98e856a63 --- /dev/null +++ b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Events/External/MediaFileDeleted.cs @@ -0,0 +1,20 @@ +using Convey.CQRS.Events; +using Convey.MessageBrokers; + +namespace MiniSpace.Services.Students.Application.Events.External +{ + [Message("mediafiles")] + public class MediaFileDeleted: IEvent + { + public Guid MediaFileId { get; } + public Guid SourceId { get; } + public string Source { get; } + + public MediaFileDeleted(Guid mediaFileId, Guid sourceId, string source) + { + MediaFileId = mediaFileId; + SourceId = sourceId; + Source = source; + } + } +} \ No newline at end of file diff --git a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Events/StudentCreated.cs b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Events/StudentCreated.cs index ec71658d1..6028518c9 100644 --- a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Events/StudentCreated.cs +++ b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Events/StudentCreated.cs @@ -6,11 +6,13 @@ public class StudentCreated : IEvent { public Guid StudentId { get; } public string FullName { get; } + public Guid MediaFileId { get; } - public StudentCreated(Guid studentId, string fullName) + public StudentCreated(Guid studentId, string fullName, Guid mediaFileId) { StudentId = studentId; FullName = fullName; + MediaFileId = mediaFileId; } } } diff --git a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Events/StudentUpdated.cs b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Events/StudentUpdated.cs index fa4324046..3968ccd09 100644 --- a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Events/StudentUpdated.cs +++ b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Application/Events/StudentUpdated.cs @@ -6,11 +6,13 @@ public class StudentUpdated : IEvent { public Guid StudentId { get; } public string FullName { get; } + public Guid MediaFileId { get; } - public StudentUpdated(Guid studentId, string fullName) + public StudentUpdated(Guid studentId, string fullName, Guid mediaFileId) { StudentId = studentId; FullName = fullName; + MediaFileId = mediaFileId; } } } diff --git a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Core/Entities/Student.cs b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Core/Entities/Student.cs index 1f19bf4c0..d183d7ad1 100644 --- a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Core/Entities/Student.cs +++ b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Core/Entities/Student.cs @@ -13,7 +13,7 @@ public class Student : AggregateRoot public string LastName { get; private set; } public string FullName => $"{FirstName} {LastName}"; public int NumberOfFriends { get; private set; } - public string ProfileImage { get; private set; } + public Guid ProfileImage { get; private set; } public string Description { get; private set; } public DateTime? DateOfBirth { get; private set; } public bool EmailNotifications { get; private set; } @@ -34,14 +34,14 @@ public IEnumerable SignedUpEvents } public Student(Guid id, string firstName, string lastName, string email, DateTime createdAt) - : this(id, email, createdAt, firstName, lastName, 0, string.Empty, string.Empty, null, + : this(id, email, createdAt, firstName, lastName, 0, Guid.Empty, string.Empty, null, false, false, false, State.Incomplete, Enumerable.Empty(), Enumerable.Empty()) { CheckFullName(firstName, lastName); } public Student(Guid id, string email, DateTime createdAt, string firstName, string lastName, - int numberOfFriends, string profileImage, string description, DateTime? dateOfBirth, + int numberOfFriends, Guid profileImage, string description, DateTime? dateOfBirth, bool emailNotifications, bool isBanned, bool isOrganizer, State state, IEnumerable interestedInEvents = null, IEnumerable signedUpEvents = null) { @@ -73,10 +73,9 @@ private void SetState(State state) AddEvent(new StudentStateChanged(this, previousState)); } - public void CompleteRegistration(string profileImage, string description, + public void CompleteRegistration(Guid profileImage, string description, DateTime dateOfBirth, DateTime now, bool emailNotifications) { - CheckProfileImage(profileImage); CheckDescription(description); CheckDateOfBirth(dateOfBirth, now); @@ -94,9 +93,8 @@ public void CompleteRegistration(string profileImage, string description, AddEvent(new StudentRegistrationCompleted(this)); } - public void Update(string profileImage, string description, bool emailNotifications) + public void Update(Guid profileImage, string description, bool emailNotifications) { - CheckProfileImage(profileImage); CheckDescription(description); if (State != State.Valid) @@ -119,14 +117,6 @@ private void CheckFullName(string firstName, string lastName) } } - private void CheckProfileImage(string profileImage) - { - if (string.IsNullOrWhiteSpace(profileImage)) - { - throw new InvalidStudentProfileImageException(Id, profileImage); - } - } - private void CheckDescription(string description) { if (string.IsNullOrWhiteSpace(description)) @@ -168,6 +158,16 @@ public void AddSignedUpEvent(Guid eventId) public void RemoveSignedUpEvent(Guid eventId) => _signedUpEvents.Remove(eventId); + + public void RemoveProfileImage(Guid mediaFileId) + { + if (ProfileImage != mediaFileId) + { + return; + } + + ProfileImage = Guid.Empty; + } public void Ban() => IsBanned = true; public void Unban() => IsBanned = false; diff --git a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Infrastructure/Mongo/Documents/StudentDocument.cs b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Infrastructure/Mongo/Documents/StudentDocument.cs index 194ef0035..694a8fd18 100644 --- a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Infrastructure/Mongo/Documents/StudentDocument.cs +++ b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Infrastructure/Mongo/Documents/StudentDocument.cs @@ -10,7 +10,7 @@ public class StudentDocument : IIdentifiable public string FirstName { get; set; } public string LastName { get; set; } public int NumberOfFriends { get; set; } - public string ProfileImage { get; set; } + public Guid ProfileImage { get; set; } public string Description { get; set; } public DateTime? DateOfBirth { get; set; } public bool EmailNotifications { get; set; } diff --git a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Infrastructure/Services/EventMapper.cs b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Infrastructure/Services/EventMapper.cs index 890c60219..f540eca2a 100644 --- a/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Infrastructure/Services/EventMapper.cs +++ b/MiniSpace.Services.Students/src/MiniSpace.Services.Students.Infrastructure/Services/EventMapper.cs @@ -15,9 +15,9 @@ public IEvent Map(IDomainEvent @event) switch (@event) { case StudentRegistrationCompleted e: - return new Application.Events.StudentCreated(e.Student.Id, e.Student.FullName); + return new Application.Events.StudentCreated(e.Student.Id, e.Student.FullName, e.Student.ProfileImage); case StudentUpdated e: - return new Application.Events.StudentUpdated(e.Student.Id, e.Student.FullName); + return new Application.Events.StudentUpdated(e.Student.Id, e.Student.FullName, e.Student.ProfileImage); case StudentStateChanged e: return new Application.Events.StudentStateChanged(e.Student.Id, e.Student.FullName, e.Student.State.ToString().ToLowerInvariant(), e.PreviousState.ToString().ToLowerInvariant()); diff --git a/MiniSpace.Web/src/MiniSpace.Web/Areas/Events/EventsService.cs b/MiniSpace.Web/src/MiniSpace.Web/Areas/Events/EventsService.cs index adfd03736..ea252e425 100644 --- a/MiniSpace.Web/src/MiniSpace.Web/Areas/Events/EventsService.cs +++ b/MiniSpace.Web/src/MiniSpace.Web/Areas/Events/EventsService.cs @@ -33,25 +33,25 @@ public Task>> GetStudentEventsAsync(Guid $"events/student/{studentId}?engagementType={engagementType}&page={page}&numberOfResults={numberOfResults}"); } - public Task> AddEventAsync(Guid eventId, string name, Guid organizerId, Guid organizationId, + public Task> CreateEventAsync(Guid eventId, string name, Guid organizerId, Guid organizationId, Guid rootOrganizationId, string startDate, string endDate, string buildingName, string street, - string buildingNumber, string apartmentNumber, string city, string zipCode, string description, - int capacity, decimal fee, string category, string publishDate) + string buildingNumber, string apartmentNumber, string city, string zipCode, IEnumerable mediaFiles, + string description, int capacity, decimal fee, string category, string publishDate) { _httpClient.SetAccessToken(_identityService.JwtDto.AccessToken); return _httpClient.PostAsync("events", new {eventId, name, organizerId, organizationId, rootOrganizationId, startDate, endDate, buildingName, street, buildingNumber, apartmentNumber, city, - zipCode, description, capacity, fee, category, publishDate}); + zipCode, mediaFiles, description, capacity, fee, category, publishDate}); } public Task> UpdateEventAsync(Guid eventId, string name, Guid organizerId, string startDate, string endDate, string buildingName, string street, string buildingNumber, string apartmentNumber, string city, string zipCode, - string description, int capacity, decimal fee, string category, string publishDate) + IEnumerable mediaFiles, string description, int capacity, decimal fee, string category, string publishDate) { _httpClient.SetAccessToken(_identityService.JwtDto.AccessToken); return _httpClient.PutAsync($"events/{eventId}", new {eventId, name, organizerId, - startDate, endDate, buildingName, street, buildingNumber, apartmentNumber, city, zipCode, description, - capacity, fee, category, publishDate}); + startDate, endDate, buildingName, street, buildingNumber, apartmentNumber, city, zipCode, mediaFiles, + description, capacity, fee, category, publishDate}); } public Task DeleteEventAsync(Guid eventId) diff --git a/MiniSpace.Web/src/MiniSpace.Web/Areas/Events/IEventsService.cs b/MiniSpace.Web/src/MiniSpace.Web/Areas/Events/IEventsService.cs index 000bfb59f..7b55fcc18 100644 --- a/MiniSpace.Web/src/MiniSpace.Web/Areas/Events/IEventsService.cs +++ b/MiniSpace.Web/src/MiniSpace.Web/Areas/Events/IEventsService.cs @@ -13,14 +13,14 @@ public interface IEventsService Task GetEventAsync(Guid eventId); Task>> GetStudentEventsAsync(Guid studentId, string engagementType, int page, int numberOfResults); - Task> AddEventAsync(Guid eventId, string name, Guid organizerId, Guid organizationId, + Task> CreateEventAsync(Guid eventId, string name, Guid organizerId, Guid organizationId, Guid rootOrganizationId, string startDate, string endDate, string buildingName, string street, - string buildingNumber, string apartmentNumber, string city, string zipCode, string description, - int capacity, decimal fee, string category, string publishDate); + string buildingNumber, string apartmentNumber, string city, string zipCode, IEnumerable mediaFiles, + string description, int capacity, decimal fee, string category, string publishDate); Task> UpdateEventAsync(Guid eventId, string name, Guid organizerId, string startDate, string endDate, string buildingName, string street, string buildingNumber, - string apartmentNumber, string city, string zipCode, string description, int capacity, decimal fee, - string category, string publishDate); + string apartmentNumber, string city, string zipCode, IEnumerable mediaFiles, string description, + int capacity, decimal fee, string category, string publishDate); Task DeleteEventAsync(Guid eventId); Task SignUpToEventAsync(Guid eventId, Guid studentId); Task CancelSignUpToEventAsync(Guid eventId, Guid studentId); diff --git a/MiniSpace.Web/src/MiniSpace.Web/Areas/Friends/FriendsService.cs b/MiniSpace.Web/src/MiniSpace.Web/Areas/Friends/FriendsService.cs index cd3684a96..92fdea873 100644 --- a/MiniSpace.Web/src/MiniSpace.Web/Areas/Friends/FriendsService.cs +++ b/MiniSpace.Web/src/MiniSpace.Web/Areas/Friends/FriendsService.cs @@ -166,7 +166,7 @@ public async Task> GetSentFriendRequestsAsync() var userDetails = await GetUserDetails(request.InviteeId); request.InviteeName = userDetails.FirstName + " " + userDetails.LastName; request.InviteeEmail = userDetails.Email; - request.InviteeImage = userDetails.ProfileImage; + //request.InviteeImage = userDetails.ProfileImage; } return friendRequests; diff --git a/MiniSpace.Web/src/MiniSpace.Web/Areas/MediaFiles/IMediaFilesService.cs b/MiniSpace.Web/src/MiniSpace.Web/Areas/MediaFiles/IMediaFilesService.cs new file mode 100644 index 000000000..10de21231 --- /dev/null +++ b/MiniSpace.Web/src/MiniSpace.Web/Areas/MediaFiles/IMediaFilesService.cs @@ -0,0 +1,16 @@ +using System; +using System.Threading.Tasks; +using MiniSpace.Web.DTO; +using MiniSpace.Web.HttpClients; + +namespace MiniSpace.Web.Areas.MediaFiles +{ + public interface IMediaFilesService + { + public Task GetFileAsync(Guid fileId); + public Task GetOriginalFileAsync(Guid fileId); + public Task> UploadMediaFileAsync(Guid sourceId, string sourceType, + Guid uploaderId, string fileName, string fileContentType, string base64Content); + public Task DeleteMediaFileAsync(Guid fileId); + } +} \ No newline at end of file diff --git a/MiniSpace.Web/src/MiniSpace.Web/Areas/MediaFiles/MediaFilesService.cs b/MiniSpace.Web/src/MiniSpace.Web/Areas/MediaFiles/MediaFilesService.cs new file mode 100644 index 000000000..c34371437 --- /dev/null +++ b/MiniSpace.Web/src/MiniSpace.Web/Areas/MediaFiles/MediaFilesService.cs @@ -0,0 +1,46 @@ +using System; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using MiniSpace.Web.Areas.Identity; +using MiniSpace.Web.DTO; +using MiniSpace.Web.HttpClients; + +namespace MiniSpace.Web.Areas.MediaFiles +{ + public class MediaFilesService : IMediaFilesService + { + private readonly IHttpClient _httpClient; + private readonly IIdentityService _identityService; + + public MediaFilesService(IHttpClient httpClient, IIdentityService identityService) + { + _httpClient = httpClient; + _identityService = identityService; + } + + public Task GetFileAsync(Guid fileId) + { + return _httpClient.GetAsync($"media-files/{fileId}"); + } + + public Task GetOriginalFileAsync(Guid fileId) + { + return _httpClient.GetAsync($"media-files/{fileId}/original"); + } + + public Task> UploadMediaFileAsync(Guid sourceId, string sourceType, Guid uploaderId, string fileName, + string fileContentType, string base64Content) + { + _httpClient.SetAccessToken(_identityService.JwtDto.AccessToken); + return _httpClient.PostAsync("media-files", new {sourceId, sourceType, uploaderId, + fileName, fileContentType, base64Content }); + } + + public Task DeleteMediaFileAsync(Guid fileId) + { + _httpClient.SetAccessToken(_identityService.JwtDto.AccessToken); + return _httpClient.DeleteAsync($"media-files/{fileId}"); + } + + } +} \ No newline at end of file diff --git a/MiniSpace.Web/src/MiniSpace.Web/Areas/Posts/IPostsService.cs b/MiniSpace.Web/src/MiniSpace.Web/Areas/Posts/IPostsService.cs index 81ce84a83..d89577bc7 100644 --- a/MiniSpace.Web/src/MiniSpace.Web/Areas/Posts/IPostsService.cs +++ b/MiniSpace.Web/src/MiniSpace.Web/Areas/Posts/IPostsService.cs @@ -11,9 +11,9 @@ public interface IPostsService Task GetPostAsync(Guid postId); Task ChangePostStateAsync(Guid postId, string state, DateTime publishDate); Task> CreatePostAsync(Guid postId, Guid eventId, Guid organizerId, string textContext, - string mediaContext, string state, DateTime? publishDate); + IEnumerable mediaFiles, string state, DateTime? publishDate); Task DeletePostAsync(Guid postId); Task> GetPostsAsync(Guid eventId); - Task> UpdatePostAsync(Guid postId, string textContent, string mediaContent); + Task> UpdatePostAsync(Guid postId, string textContent, IEnumerable mediaFiles); } } \ No newline at end of file diff --git a/MiniSpace.Web/src/MiniSpace.Web/Areas/Posts/PostsService.cs b/MiniSpace.Web/src/MiniSpace.Web/Areas/Posts/PostsService.cs index 541ad86ba..1b575c05d 100644 --- a/MiniSpace.Web/src/MiniSpace.Web/Areas/Posts/PostsService.cs +++ b/MiniSpace.Web/src/MiniSpace.Web/Areas/Posts/PostsService.cs @@ -1,4 +1,5 @@ using System; +using System.Collections; using System.Collections.Generic; using System.Threading.Tasks; using MiniSpace.Web.Areas.Identity; @@ -30,11 +31,11 @@ public Task ChangePostStateAsync(Guid postId, string state, DateTime publishDate } public Task> CreatePostAsync(Guid postId, Guid eventId, Guid organizerId, string textContent, - string mediaContext, string state, DateTime? publishDate) + IEnumerable mediaFiles, string state, DateTime? publishDate) { _httpClient.SetAccessToken(_identityService.JwtDto.AccessToken); return _httpClient.PostAsync("posts", new {postId, eventId, organizerId, textContent, - mediaContext, state, publishDate}); + mediaFiles, state, publishDate}); } public Task DeletePostAsync(Guid postId) @@ -48,10 +49,10 @@ public Task> GetPostsAsync(Guid eventId) return _httpClient.GetAsync>($"posts?eventId={eventId}"); } - public Task> UpdatePostAsync(Guid postId, string textContent, string mediaContent) + public Task> UpdatePostAsync(Guid postId, string textContent, IEnumerable mediaFiles) { _httpClient.SetAccessToken(_identityService.JwtDto.AccessToken); - return _httpClient.PutAsync($"posts/{postId}", new {postId, textContent, mediaContent}); + return _httpClient.PutAsync($"posts/{postId}", new {postId, textContent, mediaFiles}); } } } \ No newline at end of file diff --git a/MiniSpace.Web/src/MiniSpace.Web/Areas/Students/IStudentsService.cs b/MiniSpace.Web/src/MiniSpace.Web/Areas/Students/IStudentsService.cs index 1cbed12d1..0514d4e03 100644 --- a/MiniSpace.Web/src/MiniSpace.Web/Areas/Students/IStudentsService.cs +++ b/MiniSpace.Web/src/MiniSpace.Web/Areas/Students/IStudentsService.cs @@ -13,8 +13,8 @@ public interface IStudentsService void ClearStudentDto(); Task GetStudentAsync(Guid studentId); Task> GetStudentsAsync(); - Task UpdateStudentAsync(Guid studentId, string profileImage, string description, bool emailNotifications); - Task> CompleteStudentRegistrationAsync(Guid studentId, string profileImage, + Task UpdateStudentAsync(Guid studentId, Guid profileImage, string description, bool emailNotifications); + Task> CompleteStudentRegistrationAsync(Guid studentId, Guid profileImage, string description, DateTime dateOfBirth, bool emailNotifications); Task GetStudentStateAsync(Guid studentId); } diff --git a/MiniSpace.Web/src/MiniSpace.Web/Areas/Students/StudentsService.cs b/MiniSpace.Web/src/MiniSpace.Web/Areas/Students/StudentsService.cs index a5ec57ac8..d0757b6e8 100644 --- a/MiniSpace.Web/src/MiniSpace.Web/Areas/Students/StudentsService.cs +++ b/MiniSpace.Web/src/MiniSpace.Web/Areas/Students/StudentsService.cs @@ -45,14 +45,14 @@ public Task> GetStudentsAsync() return _httpClient.GetAsync>("students"); } - public Task UpdateStudentAsync(Guid studentId, string profileImage, string description, bool emailNotifications) + public Task UpdateStudentAsync(Guid studentId, Guid profileImage, string description, bool emailNotifications) { _httpClient.SetAccessToken(_identityService.JwtDto.AccessToken); return _httpClient.PutAsync($"students/{studentId}", new {studentId, profileImage, description, emailNotifications}); } - public Task> CompleteStudentRegistrationAsync(Guid studentId, string profileImage, + public Task> CompleteStudentRegistrationAsync(Guid studentId, Guid profileImage, string description, DateTime dateOfBirth, bool emailNotifications) => _httpClient.PostAsync("students", new {studentId, profileImage, description, dateOfBirth, emailNotifications}); diff --git a/MiniSpace.Web/src/MiniSpace.Web/DTO/EventDto.cs b/MiniSpace.Web/src/MiniSpace.Web/DTO/EventDto.cs index 4e4f40d1f..de41f57dd 100644 --- a/MiniSpace.Web/src/MiniSpace.Web/DTO/EventDto.cs +++ b/MiniSpace.Web/src/MiniSpace.Web/DTO/EventDto.cs @@ -12,6 +12,7 @@ public class EventDto public DateTime StartDate { get; set; } public DateTime EndDate { get; set; } public AddressDto Location { get; set; } + public IEnumerable MediaFiles { get; set; } public int InterestedStudents { get; set; } public int SignedUpStudents { get; set; } public int Capacity { get; set; } diff --git a/MiniSpace.Web/src/MiniSpace.Web/DTO/FileDto.cs b/MiniSpace.Web/src/MiniSpace.Web/DTO/FileDto.cs new file mode 100644 index 000000000..7b2075b5e --- /dev/null +++ b/MiniSpace.Web/src/MiniSpace.Web/DTO/FileDto.cs @@ -0,0 +1,17 @@ +using System; + +namespace MiniSpace.Web.DTO +{ + public class FileDto + { + public Guid MediaFileId { get; set; } + public Guid SourceId { get; set; } + public string SourceType { get; set; } + public string State { get; set; } + public DateTime CreatedAt { get; set; } + public Guid UploaderId { get; set; } + public string FileName { get; set; } + public string FileContentType { get; set; } + public string Base64Content { get; set; } + } +} \ No newline at end of file diff --git a/MiniSpace.Web/src/MiniSpace.Web/DTO/FileUploadResponseDto.cs b/MiniSpace.Web/src/MiniSpace.Web/DTO/FileUploadResponseDto.cs new file mode 100644 index 000000000..cfb4804c5 --- /dev/null +++ b/MiniSpace.Web/src/MiniSpace.Web/DTO/FileUploadResponseDto.cs @@ -0,0 +1,9 @@ +using System; + +namespace MiniSpace.Web.DTO +{ + public class FileUploadResponseDto + { + public Guid FileId { get; set; } + } +} \ No newline at end of file diff --git a/MiniSpace.Web/src/MiniSpace.Web/DTO/PostDto.cs b/MiniSpace.Web/src/MiniSpace.Web/DTO/PostDto.cs index 966461718..47961054e 100644 --- a/MiniSpace.Web/src/MiniSpace.Web/DTO/PostDto.cs +++ b/MiniSpace.Web/src/MiniSpace.Web/DTO/PostDto.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; namespace MiniSpace.Web.DTO { @@ -8,7 +9,7 @@ public class PostDto public Guid EventId { get; set; } public Guid OrganizerId { get; set; } public string TextContent { get; set; } - public string MediaContent { get; set; } + public IEnumerable MediaFiles { get; set; } public string State { get; set; } public DateTime? PublishDate { get; set; } public DateTime CreatedAt { get; set; } diff --git a/MiniSpace.Web/src/MiniSpace.Web/DTO/StudentDto.cs b/MiniSpace.Web/src/MiniSpace.Web/DTO/StudentDto.cs index 0400d6cd8..a09c52928 100644 --- a/MiniSpace.Web/src/MiniSpace.Web/DTO/StudentDto.cs +++ b/MiniSpace.Web/src/MiniSpace.Web/DTO/StudentDto.cs @@ -10,7 +10,7 @@ public class StudentDto public string FirstName { get; set; } public string LastName { get; set; } public int NumberOfFriends { get; set; } - public string ProfileImage { get; set; } + public Guid ProfileImage { get; set; } public string Description { get; set; } public DateTime DateOfBirth { get; set; } public bool EmailNotifications { get; set; } diff --git a/MiniSpace.Web/src/MiniSpace.Web/DTO/Types/MediaFileContextType.cs b/MiniSpace.Web/src/MiniSpace.Web/DTO/Types/MediaFileContextType.cs new file mode 100644 index 000000000..9cbe6b2ea --- /dev/null +++ b/MiniSpace.Web/src/MiniSpace.Web/DTO/Types/MediaFileContextType.cs @@ -0,0 +1,9 @@ +namespace MiniSpace.Web.DTO.Types +{ + public enum MediaFileContextType + { + Event, + Post, + StudentProfile, + } +} \ No newline at end of file diff --git a/MiniSpace.Web/src/MiniSpace.Web/MiniSpace.Web.csproj b/MiniSpace.Web/src/MiniSpace.Web/MiniSpace.Web.csproj index 1df37b7db..de79ee777 100644 --- a/MiniSpace.Web/src/MiniSpace.Web/MiniSpace.Web.csproj +++ b/MiniSpace.Web/src/MiniSpace.Web/MiniSpace.Web.csproj @@ -13,7 +13,7 @@ - + diff --git a/MiniSpace.Web/src/MiniSpace.Web/Models/Events/CreateEventModel.cs b/MiniSpace.Web/src/MiniSpace.Web/Models/Events/CreateEventModel.cs index cf6129ab6..f37f70062 100644 --- a/MiniSpace.Web/src/MiniSpace.Web/Models/Events/CreateEventModel.cs +++ b/MiniSpace.Web/src/MiniSpace.Web/Models/Events/CreateEventModel.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using MiniSpace.Web.DTO; namespace MiniSpace.Web.Models.Events @@ -17,6 +18,7 @@ public class CreateEventModel public string ApartmentNumber { get; set; } public string City { get; set; } public string ZipCode { get; set; } + public IEnumerable MediaFiles { get; } public string Description { get; set; } public int Capacity { get; set; } public decimal Fee { get; set; } diff --git a/MiniSpace.Web/src/MiniSpace.Web/Models/Events/UpdateEventModel.cs b/MiniSpace.Web/src/MiniSpace.Web/Models/Events/UpdateEventModel.cs index f25a8a737..fcc96338a 100644 --- a/MiniSpace.Web/src/MiniSpace.Web/Models/Events/UpdateEventModel.cs +++ b/MiniSpace.Web/src/MiniSpace.Web/Models/Events/UpdateEventModel.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; namespace MiniSpace.Web.Models.Events { @@ -15,6 +16,7 @@ public class UpdateEventModel public string ApartmentNumber { get; set; } public string City { get; set; } public string ZipCode { get; set; } + public IEnumerable MediaFiles { get; set; } public string Description { get; set; } public int Capacity { get; set; } public decimal Fee { get; set; } diff --git a/MiniSpace.Web/src/MiniSpace.Web/Models/Posts/CreatePostModel.cs b/MiniSpace.Web/src/MiniSpace.Web/Models/Posts/CreatePostModel.cs index d2316ace8..eb3da90e3 100644 --- a/MiniSpace.Web/src/MiniSpace.Web/Models/Posts/CreatePostModel.cs +++ b/MiniSpace.Web/src/MiniSpace.Web/Models/Posts/CreatePostModel.cs @@ -1,4 +1,6 @@ using System; +using System.Collections; +using System.Collections.Generic; namespace MiniSpace.Web.Models.Posts { @@ -8,7 +10,7 @@ public class CreatePostModel public Guid EventId { get; set; } public Guid OrganizerId { get; set; } public string TextContent { get; set; } - public string MediaContent { get; set; } + public IEnumerable MediaFiles { get; set; } public string State { get; set; } public DateTime PublishDate { get; set; } } diff --git a/MiniSpace.Web/src/MiniSpace.Web/Models/Posts/UpdatePostModel.cs b/MiniSpace.Web/src/MiniSpace.Web/Models/Posts/UpdatePostModel.cs index 679923924..4762b1b49 100644 --- a/MiniSpace.Web/src/MiniSpace.Web/Models/Posts/UpdatePostModel.cs +++ b/MiniSpace.Web/src/MiniSpace.Web/Models/Posts/UpdatePostModel.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; namespace MiniSpace.Web.Models.Posts { @@ -6,6 +7,6 @@ public class UpdatePostModel { public Guid PostId { get; set; } public string TextContent { get; set; } - public string MediaContent { get; set; } + public IEnumerable MediaFiles { get; set; } } } diff --git a/MiniSpace.Web/src/MiniSpace.Web/Models/Students/CompleteRegistrationModel.cs b/MiniSpace.Web/src/MiniSpace.Web/Models/Students/CompleteRegistrationModel.cs index 5938dfe67..b7b904a1e 100644 --- a/MiniSpace.Web/src/MiniSpace.Web/Models/Students/CompleteRegistrationModel.cs +++ b/MiniSpace.Web/src/MiniSpace.Web/Models/Students/CompleteRegistrationModel.cs @@ -5,7 +5,7 @@ namespace MiniSpace.Web.Models.Students public class CompleteRegistrationModel { public Guid StudentId { get; set; } - public string ProfileImage { get; set; } + public Guid ProfileImage { get; set; } public string Description { get; set; } public DateTime DateOfBirth { get; set; } public bool EmailNotifications { get; set; } diff --git a/MiniSpace.Web/src/MiniSpace.Web/Pages/Account/CompleteRegistration.razor b/MiniSpace.Web/src/MiniSpace.Web/Pages/Account/CompleteRegistration.razor index 6566fe092..d6ea78f57 100644 --- a/MiniSpace.Web/src/MiniSpace.Web/Pages/Account/CompleteRegistration.razor +++ b/MiniSpace.Web/src/MiniSpace.Web/Pages/Account/CompleteRegistration.razor @@ -5,9 +5,12 @@ @using MiniSpace.Web.Models.Students @using System.IO @using MiniSpace.Web.Areas.Http +@using MiniSpace.Web.Areas.MediaFiles +@using MiniSpace.Web.DTO.Types @using Radzen @inject IIdentityService IdentityService @inject IStudentsService StudentsService +@inject IMediaFilesService MediaFilesService @inject IErrorMapperService ErrorMapperService @inject NavigationManager NavigationManager @@ -66,7 +69,7 @@ if (IdentityService.IsAuthenticated) { completeRegistrationModel.StudentId = IdentityService.UserDto.Id; - completeRegistrationModel.ProfileImage = "null"; + completeRegistrationModel.ProfileImage = Guid.Empty; completeRegistrationModel.DateOfBirth = DateTime.Now; } } @@ -118,7 +121,14 @@ long maxFileSize = 10 * 1024 * 1024; var stream = file.OpenReadStream(maxFileSize); byte[] bytes = await ReadFully(stream); - completeRegistrationModel.ProfileImage = Convert.ToBase64String(bytes); + var base64Content = Convert.ToBase64String(bytes); + var response = await MediaFilesService.UploadMediaFileAsync(IdentityService.UserDto.Id, + MediaFileContextType.StudentProfile.ToString(), IdentityService.UserDto.Id, + file.Name, file.ContentType, base64Content); + if (response.Content != null && response.Content.FileId != Guid.Empty) + { + completeRegistrationModel.ProfileImage = response.Content.FileId; + } stream.Close(); } catch (Exception ex) diff --git a/MiniSpace.Web/src/MiniSpace.Web/Pages/Account/ShowAccount.razor b/MiniSpace.Web/src/MiniSpace.Web/Pages/Account/ShowAccount.razor index 9e320d9df..c474547e9 100644 --- a/MiniSpace.Web/src/MiniSpace.Web/Pages/Account/ShowAccount.razor +++ b/MiniSpace.Web/src/MiniSpace.Web/Pages/Account/ShowAccount.razor @@ -3,9 +3,12 @@ @using MiniSpace.Web.Components @using MiniSpace.Web.DTO @using System.IO +@using MiniSpace.Web.Areas.MediaFiles +@using MiniSpace.Web.DTO.Types @using Radzen @inject IIdentityService IdentityService @inject IStudentsService StudentsService +@inject IMediaFilesService MediaFilesService @inject NavigationManager NavigationManager

Your account

@@ -14,7 +17,7 @@ { - @@ -71,6 +74,15 @@ + + @if (isUploading) + { + + + + } + @@ -88,6 +100,9 @@ else @code { private StudentDto studentDto = new(); private bool editionDisabled = true; + private string profileImage = string.Empty; + private TaskCompletionSource clientChangeCompletionSource; + private bool isUploading = false; protected override async Task OnInitializedAsync() { @@ -95,6 +110,12 @@ else { await StudentsService.UpdateStudentDto(IdentityService.UserDto.Id); studentDto = StudentsService.StudentDto; + StateHasChanged(); + if (studentDto.ProfileImage != Guid.Empty) + { + var imageResponse = await MediaFilesService.GetFileAsync(studentDto.ProfileImage); + profileImage = imageResponse.Base64Content; + } } } @@ -106,6 +127,10 @@ else private async Task HandleUpdateStudent() { + if (clientChangeCompletionSource != null) + { + await clientChangeCompletionSource.Task; + } editionDisabled = true; await StudentsService.UpdateStudentAsync(studentDto.Id, studentDto.ProfileImage, studentDto.Description, studentDto.EmailNotifications); @@ -115,22 +140,40 @@ else async void OnClientChange(UploadChangeEventArgs args) { Console.WriteLine("Client-side upload changed"); + clientChangeCompletionSource = new TaskCompletionSource(); foreach (var file in args.Files) { Console.WriteLine($"File: {file.Name} / {file.Size} bytes"); + isUploading = true; + StateHasChanged(); try { long maxFileSize = 10 * 1024 * 1024; var stream = file.OpenReadStream(maxFileSize); byte[] bytes = await ReadFully(stream); - studentDto.ProfileImage = Convert.ToBase64String(bytes); + var base64Content = Convert.ToBase64String(bytes); + var response = await MediaFilesService.UploadMediaFileAsync(IdentityService.UserDto.Id, + MediaFileContextType.StudentProfile.ToString(), IdentityService.UserDto.Id, + file.Name, file.ContentType, base64Content); + if (response.Content != null && response.Content.FileId != Guid.Empty) + { + studentDto.ProfileImage = response.Content.FileId; + } + stream.Close(); + clientChangeCompletionSource.SetResult(true); } catch (Exception ex) - { + { Console.WriteLine($"Client-side file read error: {ex.Message}"); + clientChangeCompletionSource.SetResult(false); + } + finally + { + isUploading = false; + StateHasChanged(); } } } diff --git a/MiniSpace.Web/src/MiniSpace.Web/Pages/Events/EventCreate.razor b/MiniSpace.Web/src/MiniSpace.Web/Pages/Events/EventCreate.razor index bb1dec9ad..be8e71706 100644 --- a/MiniSpace.Web/src/MiniSpace.Web/Pages/Events/EventCreate.razor +++ b/MiniSpace.Web/src/MiniSpace.Web/Pages/Events/EventCreate.razor @@ -2,13 +2,17 @@ @using MiniSpace.Web.Areas.Identity @using MiniSpace.Web.Areas.Events @using MiniSpace.Web.Areas.Http +@using MiniSpace.Web.Areas.MediaFiles @using MiniSpace.Web.Areas.Organizations @using MiniSpace.Web.DTO +@using MiniSpace.Web.DTO.Types @using MiniSpace.Web.Models.Events @using Radzen +@using System.IO @inject IIdentityService IdentityService @inject IEventsService EventsService @inject IOrganizationsService OrganizationsService +@inject IMediaFilesService MediaFilesService @inject IErrorMapperService ErrorMapperService @inject NavigationManager NavigationManager @@ -79,16 +83,16 @@ - - - - - - - + + + + + + + - + + + + + + + @if (isUploading) + { + + + + } + + + Choose files to upload (max 5) + + + + + + @@ -159,6 +185,9 @@ private Guid organizerId; private bool pageInitialized = false; private bool organizationsFound = false; + private TaskCompletionSource clientChangeCompletionSource; + private bool isUploading = false; + private Dictionary images = new (); private CreateEventModel _createEventModel = new() { @@ -211,6 +240,7 @@ if (organizations.Any()) { organizationsFound = true; + _createEventModel.EventId = Guid.NewGuid(); _createEventModel.OrganizerId = organizerId; _createEventModel.Organization = organizations.First(); } @@ -221,12 +251,12 @@ private async Task HandleCreateEvent() { - var response = await EventsService.AddEventAsync(Guid.Empty, _createEventModel.Name, + var response = await EventsService.CreateEventAsync(_createEventModel.EventId, _createEventModel.Name, _createEventModel.OrganizerId, _createEventModel.Organization.Id, _createEventModel.Organization.RootId, _createEventModel.StartDate.ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ss.fffZ"), _createEventModel.EndDate.ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ss.fffZ"), _createEventModel.BuildingName, _createEventModel.Street, _createEventModel.BuildingNumber, - _createEventModel.ApartmentNumber, _createEventModel.City, _createEventModel.ZipCode, + _createEventModel.ApartmentNumber, _createEventModel.City, _createEventModel.ZipCode, images.Select(o => o.Value), _createEventModel.Description, _createEventModel.Capacity, _createEventModel.Fee, _createEventModel.Category, publishInfo == 2 ? _createEventModel.PublishDate.ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ss.fffZ") : string.Empty); @@ -240,4 +270,62 @@ NavigationManager.NavigateTo("/events/organize"); } } + + async void OnClientChange(UploadChangeEventArgs args) + { + Console.WriteLine("Client-side upload changed"); + clientChangeCompletionSource = new TaskCompletionSource(); + var uploadedImages = new Dictionary(); + isUploading = true; + + foreach (var file in args.Files) + { + StateHasChanged(); + if (images.TryGetValue(file.Name, out var imageId)) + { + uploadedImages.Add(file.Name, imageId); + continue; + } + + try + { + long maxFileSize = 10 * 1024 * 1024; + var stream = file.OpenReadStream(maxFileSize); + byte[] bytes = await ReadFully(stream); + var base64Content = Convert.ToBase64String(bytes); + var response = await MediaFilesService.UploadMediaFileAsync(_createEventModel.EventId, + MediaFileContextType.Event.ToString(), IdentityService.UserDto.Id, + file.Name, file.ContentType, base64Content); + if (response.Content != null && response.Content.FileId != Guid.Empty) + { + uploadedImages.Add(file.Name, response.Content.FileId); + } + stream.Close(); + } + catch (Exception ex) + { + Console.WriteLine($"Client-side file read error: {ex.Message}"); + } + finally + { + + } + } + isUploading = false; + StateHasChanged(); + images = uploadedImages; + clientChangeCompletionSource.SetResult(true); + } + + private static async Task ReadFully(Stream input) + { + byte[] buffer = new byte[16*1024]; + using MemoryStream ms = new MemoryStream(); + int read; + while ((read = await input.ReadAsync(buffer, 0, buffer.Length)) > 0) + { + ms.Write(buffer, 0, read); + } + return ms.ToArray(); + } } diff --git a/MiniSpace.Web/src/MiniSpace.Web/Pages/Events/EventUpdate.razor b/MiniSpace.Web/src/MiniSpace.Web/Pages/Events/EventUpdate.razor index cf3d23504..5d97365d9 100644 --- a/MiniSpace.Web/src/MiniSpace.Web/Pages/Events/EventUpdate.razor +++ b/MiniSpace.Web/src/MiniSpace.Web/Pages/Events/EventUpdate.razor @@ -195,6 +195,7 @@ updateEventModel.ApartmentNumber = eventDto.Location.ApartmentNumber; updateEventModel.City = eventDto.Location.City; updateEventModel.ZipCode = eventDto.Location.ZipCode; + updateEventModel.MediaFiles = eventDto.MediaFiles; updateEventModel.Description = eventDto.Description; updateEventModel.Capacity = eventDto.Capacity; updateEventModel.Fee = eventDto.Fee; @@ -211,7 +212,7 @@ updateEventModel.StartDate.ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ss.fffZ"), updateEventModel.EndDate.ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ss.fffZ"), updateEventModel.BuildingName, updateEventModel.Street, updateEventModel.BuildingNumber, - updateEventModel.ApartmentNumber, updateEventModel.City, updateEventModel.ZipCode, + updateEventModel.ApartmentNumber, updateEventModel.City, updateEventModel.ZipCode, updateEventModel.MediaFiles, updateEventModel.Description, updateEventModel.Capacity, updateEventModel.Fee, updateEventModel.Category, publishInfo == 2 ? updateEventModel.PublishDate.ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ss.fffZ") : string.Empty); diff --git a/MiniSpace.Web/src/MiniSpace.Web/Pages/Friends/Friends.razor b/MiniSpace.Web/src/MiniSpace.Web/Pages/Friends/Friends.razor index 633ea8d04..96d71a868 100644 --- a/MiniSpace.Web/src/MiniSpace.Web/Pages/Friends/Friends.razor +++ b/MiniSpace.Web/src/MiniSpace.Web/Pages/Friends/Friends.razor @@ -20,7 +20,7 @@ foreach (var friend in friends) {
- Friend Image + @*Friend Image*@
@friend.StudentDetails.FirstName @friend.StudentDetails.LastName

@friend.StudentDetails.Email

diff --git a/MiniSpace.Web/src/MiniSpace.Web/Pages/Friends/FriendsSearch.razor b/MiniSpace.Web/src/MiniSpace.Web/Pages/Friends/FriendsSearch.razor index 32213280c..8229792f8 100644 --- a/MiniSpace.Web/src/MiniSpace.Web/Pages/Friends/FriendsSearch.razor +++ b/MiniSpace.Web/src/MiniSpace.Web/Pages/Friends/FriendsSearch.razor @@ -7,7 +7,9 @@ @inject IFriendsService FriendsService @using MiniSpace.Web.DTO @using MiniSpace.Web.Areas.Identity +@using MiniSpace.Web.Areas.MediaFiles @inject IIdentityService IdentityService +@inject IMediaFilesService MediaFilesService @inject Radzen.NotificationService NotificationService @inject IJSRuntime JSRuntime @@ -22,7 +24,10 @@ @foreach (var student in students) {
- Student Image + @if(images.ContainsKey(student.Id)) + { + Student Image + }
@student.FirstName @student.LastName

Email: @student.Email

@@ -60,7 +65,7 @@
- Profile Image + Profile Image

@student?.FirstName @student?.LastName

    @@ -254,11 +259,26 @@ private int currentPage = 1; private int pageSize = 10; private int totalStudents; + private Dictionary images = new (); protected override async Task OnInitializedAsync() { - sentRequests = await FriendsService.GetSentFriendRequestsAsync(); + sentRequests = await FriendsService.GetSentFriendRequestsAsync(); await LoadStudents(); StateHasChanged(); + + var tasks = new List(); + foreach (var student in students) + { + tasks.Add(FetchImageAsync(student)); + } + + await Task.WhenAll(tasks); + } + + private async Task FetchImageAsync(StudentDto student) + { + var result = await MediaFilesService.GetFileAsync(student.ProfileImage); + images.Add(student.Id, result.Base64Content); } @* private async Task LoadStudents() { @@ -306,6 +326,10 @@ } private void SearchFriends() { + if (string.IsNullOrWhiteSpace(searchTerm)) + { + return; + } students = students.Where(s => s.FirstName.Contains(searchTerm, StringComparison.OrdinalIgnoreCase) || s.LastName.Contains(searchTerm, StringComparison.OrdinalIgnoreCase)).ToList(); } @@ -321,13 +345,13 @@ var currentUserId = IdentityService.GetCurrentUserId(); await FriendsService.InviteStudent(currentUserId, studentId); - var student = students.FirstOrDefault(s => s.Id == studentId); + var student = students.FirstOrDefault(s => s.Id == studentId); if (student != null) { student.InvitationSent = true; student.IsInvitationPending = true; } - sentRequests = await FriendsService.GetSentFriendRequestsAsync(); + sentRequests = await FriendsService.GetSentFriendRequestsAsync(); NotificationService.Notify(Radzen.NotificationSeverity.Success, "Invitation Sent", "The invitation has been successfully sent.", 10000); await JSRuntime.InvokeVoidAsync("playNotificationSound"); StateHasChanged(); diff --git a/MiniSpace.Web/src/MiniSpace.Web/Pages/Posts/PostCreate.razor b/MiniSpace.Web/src/MiniSpace.Web/Pages/Posts/PostCreate.razor index c7901269f..059cc6c6b 100644 --- a/MiniSpace.Web/src/MiniSpace.Web/Pages/Posts/PostCreate.razor +++ b/MiniSpace.Web/src/MiniSpace.Web/Pages/Posts/PostCreate.razor @@ -1,12 +1,16 @@ @page "/events/{EventId}/posts/create" @using MiniSpace.Web.Areas.Identity @using MiniSpace.Web.Areas.Http +@using MiniSpace.Web.Areas.MediaFiles @using MiniSpace.Web.Areas.Posts @using MiniSpace.Web.DTO +@using MiniSpace.Web.DTO.Types @using MiniSpace.Web.Models.Posts @using Radzen +@using System.IO @inject IIdentityService IdentityService @inject IPostsService PostsService +@inject IMediaFilesService MediaFilesService @inject IErrorMapperService ErrorMapperService @inject NavigationManager NavigationManager @@ -50,6 +54,25 @@ } + + + + + @if (isUploading) + { + + + + } + + + Choose files to upload (max 3) + + + + @@ -72,11 +95,13 @@ private CreatePostModel createPostModel = new() { TextContent = "Lorem ipsum!", - MediaContent = "" }; private bool showError = false; private string errorMessage = string.Empty; private int publishInfo = 1; + private TaskCompletionSource clientChangeCompletionSource; + private bool isUploading = false; + private Dictionary images = new (); private static bool ValidateDate(DateTime dateTime) { @@ -88,7 +113,7 @@ if (IdentityService.IsAuthenticated && IdentityService.GetCurrentUserRole() == "organizer") { organizerId = IdentityService.GetCurrentUserId(); - + createPostModel.PostId = Guid.NewGuid(); createPostModel.EventId = new Guid(EventId); createPostModel.OrganizerId = organizerId; } @@ -96,8 +121,12 @@ private async Task HandleCreatePost() { - var response = await PostsService.CreatePostAsync(Guid.Empty, createPostModel.EventId, - createPostModel.OrganizerId, createPostModel.TextContent, createPostModel.MediaContent, + if (clientChangeCompletionSource != null) + { + await clientChangeCompletionSource.Task; + } + var response = await PostsService.CreatePostAsync(createPostModel.PostId, createPostModel.EventId, + createPostModel.OrganizerId, createPostModel.TextContent, images.Select(i => i.Value), publishInfo == 2 ? "ToBePublished" : "Published", publishInfo == 2 ? createPostModel.PublishDate.ToUniversalTime() : null); @@ -111,4 +140,62 @@ NavigationManager.NavigateTo($"/events/{EventId}"); } } + + async void OnClientChange(UploadChangeEventArgs args) + { + Console.WriteLine("Client-side upload changed"); + clientChangeCompletionSource = new TaskCompletionSource(); + var uploadedImages = new Dictionary(); + isUploading = true; + + foreach (var file in args.Files) + { + StateHasChanged(); + if (images.TryGetValue(file.Name, out var imageId)) + { + uploadedImages.Add(file.Name, imageId); + continue; + } + + try + { + long maxFileSize = 10 * 1024 * 1024; + var stream = file.OpenReadStream(maxFileSize); + byte[] bytes = await ReadFully(stream); + var base64Content = Convert.ToBase64String(bytes); + var response = await MediaFilesService.UploadMediaFileAsync(createPostModel.PostId, + MediaFileContextType.Post.ToString(), IdentityService.UserDto.Id, + file.Name, file.ContentType, base64Content); + if (response.Content != null && response.Content.FileId != Guid.Empty) + { + uploadedImages.Add(file.Name, response.Content.FileId); + } + stream.Close(); + } + catch (Exception ex) + { + Console.WriteLine($"Client-side file read error: {ex.Message}"); + } + finally + { + + } + } + isUploading = false; + StateHasChanged(); + images = uploadedImages; + clientChangeCompletionSource.SetResult(true); + } + + private static async Task ReadFully(Stream input) + { + byte[] buffer = new byte[16*1024]; + using MemoryStream ms = new MemoryStream(); + int read; + while ((read = await input.ReadAsync(buffer, 0, buffer.Length)) > 0) + { + ms.Write(buffer, 0, read); + } + return ms.ToArray(); + } } diff --git a/MiniSpace.Web/src/MiniSpace.Web/Pages/Posts/PostUpdate.razor b/MiniSpace.Web/src/MiniSpace.Web/Pages/Posts/PostUpdate.razor index e0fac9bec..0d92ed9dd 100644 --- a/MiniSpace.Web/src/MiniSpace.Web/Pages/Posts/PostUpdate.razor +++ b/MiniSpace.Web/src/MiniSpace.Web/Pages/Posts/PostUpdate.razor @@ -82,7 +82,7 @@ postDto = await PostsService.GetPostAsync(new Guid(PostId)); updatePostModel.PostId = postDto.Id; updatePostModel.TextContent = postDto.TextContent; - updatePostModel.MediaContent = postDto.MediaContent; + updatePostModel.MediaFiles = postDto.MediaFiles; } pageInitialized = true; @@ -91,7 +91,7 @@ private async Task HandleUpdatePost() { var response = await PostsService.UpdatePostAsync(updatePostModel.PostId, - updatePostModel.TextContent, updatePostModel.MediaContent); + updatePostModel.TextContent, updatePostModel.MediaFiles); if (response.ErrorMessage != null) { diff --git a/MiniSpace.Web/src/MiniSpace.Web/Startup.cs b/MiniSpace.Web/src/MiniSpace.Web/Startup.cs index b03d42617..a7ff08313 100644 --- a/MiniSpace.Web/src/MiniSpace.Web/Startup.cs +++ b/MiniSpace.Web/src/MiniSpace.Web/Startup.cs @@ -23,6 +23,7 @@ using MiniSpace.Web.Areas.Friends; using Microsoft.AspNetCore.Components.Authorization; using Blazored.LocalStorage; +using MiniSpace.Web.Areas.MediaFiles; using MiniSpace.Web.Areas.Reactions; namespace MiniSpace.Web @@ -66,6 +67,7 @@ public void ConfigureServices(IServiceCollection services) services.AddScoped(); services.AddScoped(); services.AddScoped(); + services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); diff --git a/MiniSpace/compose/services.yml b/MiniSpace/compose/services.yml index 86000600e..551033ebe 100644 --- a/MiniSpace/compose/services.yml +++ b/MiniSpace/compose/services.yml @@ -75,6 +75,15 @@ services: networks: - minispace + mediafiles-service: + image: adrianvsaint/minispace.services.mediafiles:latest + container_name: mediafiles-service + restart: unless-stopped + ports: + - 5014:80 + networks: + - minispace + organizations-service: image: adrianvsaint/minispace.services.organizations:latest container_name: organizations-service diff --git a/MiniSpace/scripts/dockerize-all.sh b/MiniSpace/scripts/dockerize-all.sh index f2889fe36..1a5fca3e2 100755 --- a/MiniSpace/scripts/dockerize-all.sh +++ b/MiniSpace/scripts/dockerize-all.sh @@ -10,6 +10,7 @@ directories=( "MiniSpace.Services.Reactions" "MiniSpace.Services.Posts" "MiniSpace.Services.Comments" + "MiniSpace.Services.MediaFiles" "MiniSpace.Services.Organizations" )