From 9afd0931a698927079e6239543a310aa54f5c852 Mon Sep 17 00:00:00 2001 From: soyalper Date: Fri, 23 Aug 2024 17:49:30 +0300 Subject: [PATCH] AH-16 API image upload --- API/Controllers/AccountController.cs | 6 +- API/Controllers/PhotosController.cs | 27 ++ API/Controllers/ProfilesController.cs | 13 + .../ApplicationServiceExtensions.cs | 3 + API/activity-hub.db | Bin 4096 -> 135168 bytes API/activity-hub.db-shm | Bin 32768 -> 32768 bytes API/activity-hub.db-wal | Bin 461472 -> 181312 bytes Application/Application.csproj | 3 + Application/Core/MappingProfiles.cs | 9 +- .../Activities/Contracts/ActivityDto.cs | 6 +- .../Attendance/Contracts/AttendeeDto.cs} | 4 +- .../Commands/AddPhoto/AddPhotoCommand.cs | 11 + .../Commands/AddPhoto/AddPhotoHandler.cs | 36 ++ .../Commands/AddPhoto/PhotoUploadResult.cs | 7 + .../DeletePhoto/DeletePhotoCommand.cs | 9 + .../DeletePhoto/DeletePhotoHandler.cs | 35 ++ .../SetMainPhoto/SetMainPhotoCommand.cs | 9 + .../SetMainPhoto/SetMainPhotoHandler.cs | 31 ++ .../Features/Profiles/Contracts/Profile.cs | 12 + .../Queries/GetProfile/GetProfileHandler.cs | 21 + .../Queries/GetProfile/GetProfileQuery.cs | 10 + Application/Interfaces/IPhotoAccessor.cs | 10 + Domain/Entities/Photo.cs | 8 + Domain/Entities/User.cs | 1 + Infrastructure/Infrastructure.csproj | 4 + Infrastructure/Photos/CloudinarySettings.cs | 8 + Infrastructure/Photos/PhotoAccessor.cs | 55 +++ Persistence/DataContext.cs | 1 + ...0240823124603_PhotoEntityAdded.Designer.cs | 383 ++++++++++++++++++ .../20240823124603_PhotoEntityAdded.cs | 45 ++ .../Migrations/DataContextModelSnapshot.cs | 30 ++ 31 files changed, 788 insertions(+), 9 deletions(-) create mode 100644 API/Controllers/PhotosController.cs create mode 100644 API/Controllers/ProfilesController.cs rename Application/{Profiles/Profile.cs => Features/Attendance/Contracts/AttendeeDto.cs} (67%) create mode 100644 Application/Features/Photos/Commands/AddPhoto/AddPhotoCommand.cs create mode 100644 Application/Features/Photos/Commands/AddPhoto/AddPhotoHandler.cs create mode 100644 Application/Features/Photos/Commands/AddPhoto/PhotoUploadResult.cs create mode 100644 Application/Features/Photos/Commands/DeletePhoto/DeletePhotoCommand.cs create mode 100644 Application/Features/Photos/Commands/DeletePhoto/DeletePhotoHandler.cs create mode 100644 Application/Features/Photos/Commands/SetMainPhoto/SetMainPhotoCommand.cs create mode 100644 Application/Features/Photos/Commands/SetMainPhoto/SetMainPhotoHandler.cs create mode 100644 Application/Features/Profiles/Contracts/Profile.cs create mode 100644 Application/Features/Profiles/Queries/GetProfile/GetProfileHandler.cs create mode 100644 Application/Features/Profiles/Queries/GetProfile/GetProfileQuery.cs create mode 100644 Application/Interfaces/IPhotoAccessor.cs create mode 100644 Domain/Entities/Photo.cs create mode 100644 Infrastructure/Photos/CloudinarySettings.cs create mode 100644 Infrastructure/Photos/PhotoAccessor.cs create mode 100644 Persistence/Migrations/20240823124603_PhotoEntityAdded.Designer.cs create mode 100644 Persistence/Migrations/20240823124603_PhotoEntityAdded.cs diff --git a/API/Controllers/AccountController.cs b/API/Controllers/AccountController.cs index 9f7e1c8..4c62422 100644 --- a/API/Controllers/AccountController.cs +++ b/API/Controllers/AccountController.cs @@ -18,7 +18,7 @@ public class AccountController(UserManager userManager, TokenService token [HttpPost("login")] public async Task> Login(LoginDto loginDto) { - var user = await userManager.FindByEmailAsync(loginDto.Email); + var user = await userManager.Users.Include(p => p.Photos).FirstOrDefaultAsync(u => u.Email == loginDto.Email); if (user == null) return Unauthorized(); @@ -65,7 +65,7 @@ public async Task> Register(RegisterDto registerDto) [HttpGet] public async Task> GetCurrentUser() { - var user = await userManager.FindByEmailAsync(User.FindFirstValue(ClaimTypes.Email)); + var user = await userManager.Users.Include(p => p.Photos).FirstOrDefaultAsync(u => u.Email == User.FindFirstValue(ClaimTypes.Email)); return CreateUserObject(user); } @@ -75,7 +75,7 @@ private AuthUserDto CreateUserObject(User user) return new AuthUserDto { DisplayName = user.DisplayName, - Image = null, + Image = user.Photos.FirstOrDefault(x => x.IsMain)?.Url, Token = tokenService.CreateToken(user), Username = user.UserName }; diff --git a/API/Controllers/PhotosController.cs b/API/Controllers/PhotosController.cs new file mode 100644 index 0000000..08f85fa --- /dev/null +++ b/API/Controllers/PhotosController.cs @@ -0,0 +1,27 @@ +using Application.Features.Photos.Commands.AddPhoto; +using Application.Features.Photos.Commands.DeletePhoto; +using Application.Features.Photos.Commands.SetMainPhoto; +using Microsoft.AspNetCore.Mvc; + +namespace API.Controllers; + +public class PhotosController : BaseApiController +{ + [HttpPost] + public async Task Add([FromForm] AddPhotoCommand command) + { + return HandleResult(await Mediator.Send(command)); + } + + [HttpDelete("{id}")] + public async Task Delete(string id) + { + return HandleResult(await Mediator.Send(new DeletePhotoCommand { Id = id })); + } + + [HttpPost("{id}/setMain")] + public async Task SetMain(string id) + { + return HandleResult(await Mediator.Send(new SetMainPhotoCommand { Id = id })); + } +} diff --git a/API/Controllers/ProfilesController.cs b/API/Controllers/ProfilesController.cs new file mode 100644 index 0000000..00607ce --- /dev/null +++ b/API/Controllers/ProfilesController.cs @@ -0,0 +1,13 @@ +using Application.Features.Profiles.Queries.GetProfile; +using Microsoft.AspNetCore.Mvc; + +namespace API.Controllers; + +public class ProfilesController : BaseApiController +{ + [HttpGet("{username}")] + public async Task GetProfile(string username) + { + return HandleResult(await Mediator.Send(new GetProfileQuery { Username = username })); + } +} diff --git a/API/Extensions/ApplicationServiceExtensions.cs b/API/Extensions/ApplicationServiceExtensions.cs index 0a96f86..c7ee7d9 100644 --- a/API/Extensions/ApplicationServiceExtensions.cs +++ b/API/Extensions/ApplicationServiceExtensions.cs @@ -4,6 +4,7 @@ using Application.Interfaces; using FluentValidation; using FluentValidation.AspNetCore; +using Infrastructure.Photos; using Infrastructure.Security; using Microsoft.EntityFrameworkCore; using Persistence; @@ -26,6 +27,8 @@ public static IServiceCollection AddApplicationServices(this IServiceCollection services.AddValidatorsFromAssemblyContaining(); services.AddHttpContextAccessor(); services.AddScoped(); + services.AddScoped(); + services.Configure(config.GetSection("Cloudinary")); return services; } diff --git a/API/activity-hub.db b/API/activity-hub.db index 9a472209435d88229f65ab7a0ca30d67df87a462..919aad1970fe0bc65bb059c18d10565b846de99a 100644 GIT binary patch literal 135168 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 diff --git a/API/activity-hub.db-shm b/API/activity-hub.db-shm index 43c15b675b8ea941d409ff278d6077f8ce75f213..1126f28b7ae5df29cdeb17457a36378bda807050 100644 GIT binary patch literal 32768 zcmeI*H%>$`5C-6%9G0*wVV9i4l4@!sI&MS3DYyzPH60BXfC7n*15kq>LI_r(Ky<5b zq>=qBYixf9nAdtfN)=Njdy@KL8P`ibwI6REPA^_|n&)S)bH|sBo2!%B-TigzkNZoh zkErUteT&cKuVJ;!)=E}qch3K_)>-SfGyb|>t|zL}l0 zQ*|B}1iDk8Hv<{XWMD2LS>E2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjZNUEm$;0WXyR literal 32768 zcmeI**>X%#6vpu{2@**VgtQ?|o2LjOh9E)A#5~V4F+YH}aHC3HS|-U!5NIIs5G21FTi})~BeYPLtI2#@C|r`Qz6Q1E0pezb}>Eed!(? zA8juV4gctWTbST_Kk9eIbAMLUpP&C;ytkU27N_#_G=HD&%y3$rHfN^O?sPbv&MarP zGso$2<~sA7`A)a9z**?@I0a{sv)EbURPGh)!lBm*gUbKnIpw{}_x|sD`Q0m@SE8Qz=kXpSEgnD!0t(UeQ01YAk`3+NC`j)&U*T zQJv73&gz0L>zZ!rjvnc$o;TKkt3FM14y77PV3l@juSRrGhjmORbz0|iQCDON`)1eL8tZh}_&yDpbuv$afr~Mk$5gpelozZz+(pBBiZQauoJ=4qjd+D0I zQKvSlUjsGSn@tg@tw8iirw~X}ASMJ*2&5?xvoRK+H3x5J*!XCjL?gq$$v&^=TfQQzPIh?<-KqB=8HV$3?OL diff --git a/API/activity-hub.db-wal b/API/activity-hub.db-wal index c30741a56bedb701825f3476e0cfde95133b4da5..1240b4adb5bd168063c9613cd5ade3fc23d690be 100644 GIT binary patch literal 181312 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 literal 461472 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 diff --git a/Application/Application.csproj b/Application/Application.csproj index b1fc44d..0321b46 100644 --- a/Application/Application.csproj +++ b/Application/Application.csproj @@ -13,6 +13,9 @@ + + + diff --git a/Application/Core/MappingProfiles.cs b/Application/Core/MappingProfiles.cs index 097a019..0c4c04f 100644 --- a/Application/Core/MappingProfiles.cs +++ b/Application/Core/MappingProfiles.cs @@ -1,4 +1,5 @@ using Application.Features.Activities.Contracts; +using Application.Features.Attendance.Contracts; using AutoMapper; using Domain.Entities; @@ -14,9 +15,13 @@ public MappingProfiles() .ForMember(d => d.HostUsername, o => o.MapFrom(s => s.Attendees .FirstOrDefault(x => x.IsHost).User.UserName)); - CreateMap() + CreateMap() .ForMember(d => d.DisplayName, o => o.MapFrom(s => s.User.DisplayName)) .ForMember(d => d.Username, o => o.MapFrom(s => s.User.UserName)) - .ForMember(d => d.Bio, o => o.MapFrom(s => s.User.Bio)); + .ForMember(d => d.Bio, o => o.MapFrom(s => s.User.Bio)) + .ForMember(d => d.Image, o => o.MapFrom(s => s.User.Photos.FirstOrDefault(x => x.IsMain).Url)); + + CreateMap() + .ForMember(d => d.Image, s => s.MapFrom(o => o.Photos.FirstOrDefault(x => x.IsMain).Url)); } } diff --git a/Application/Features/Activities/Contracts/ActivityDto.cs b/Application/Features/Activities/Contracts/ActivityDto.cs index 018020d..2c584b7 100644 --- a/Application/Features/Activities/Contracts/ActivityDto.cs +++ b/Application/Features/Activities/Contracts/ActivityDto.cs @@ -1,4 +1,6 @@ -using Application.Profiles; +using Application.Features.Attendance.Contracts; +using Application.Features.Profiles; +using Application.Features.Profiles.Contracts; namespace Application.Features.Activities.Contracts; @@ -13,5 +15,5 @@ public class ActivityDto public string Venue { get; set; } public string HostUsername { get; set; } public bool IsCancelled { get; set; } - public ICollection Attendees { get; set; } + public ICollection Attendees { get; set; } } diff --git a/Application/Profiles/Profile.cs b/Application/Features/Attendance/Contracts/AttendeeDto.cs similarity index 67% rename from Application/Profiles/Profile.cs rename to Application/Features/Attendance/Contracts/AttendeeDto.cs index e0acec7..0a8e1cb 100644 --- a/Application/Profiles/Profile.cs +++ b/Application/Features/Attendance/Contracts/AttendeeDto.cs @@ -1,6 +1,6 @@ -namespace Application.Profiles; +namespace Application.Features.Attendance.Contracts; -public class Profile +public class AttendeeDto { public string Username { get; set; } public string DisplayName { get; set; } diff --git a/Application/Features/Photos/Commands/AddPhoto/AddPhotoCommand.cs b/Application/Features/Photos/Commands/AddPhoto/AddPhotoCommand.cs new file mode 100644 index 0000000..e4f0158 --- /dev/null +++ b/Application/Features/Photos/Commands/AddPhoto/AddPhotoCommand.cs @@ -0,0 +1,11 @@ +using Application.Core; +using Domain.Entities; +using MediatR; +using Microsoft.AspNetCore.Http; + +namespace Application.Features.Photos.Commands.AddPhoto; + +public class AddPhotoCommand : IRequest> +{ + public IFormFile File { get; set; } +} diff --git a/Application/Features/Photos/Commands/AddPhoto/AddPhotoHandler.cs b/Application/Features/Photos/Commands/AddPhoto/AddPhotoHandler.cs new file mode 100644 index 0000000..efacd2d --- /dev/null +++ b/Application/Features/Photos/Commands/AddPhoto/AddPhotoHandler.cs @@ -0,0 +1,36 @@ +using Application.Core; +using Application.Interfaces; +using Domain.Entities; +using MediatR; +using Microsoft.EntityFrameworkCore; +using Persistence; + +namespace Application.Features.Photos.Commands.AddPhoto; + +public class AddPhotoHandler(DataContext context, IPhotoAccessor photoAccessor, IUserAccessor userAccessor) + : IRequestHandler> +{ + public async Task> Handle(AddPhotoCommand request, CancellationToken cancellationToken) + { + var user = await context.Users.Include(p => p.Photos) + .FirstOrDefaultAsync(x => x.UserName == userAccessor.GetUsername()); + + if (user == null) return null; + + var photoUploadResult = await photoAccessor.AddPhoto(request.File); + + var photo = new Photo + { + Url = photoUploadResult.Url, + Id = photoUploadResult.PublicId + }; + + if (!user.Photos.Any(x => x.IsMain)) photo.IsMain = true; + + user.Photos.Add(photo); + + var result = await context.SaveChangesAsync() > 0; + + return result ? Result.Success(photo) : Result.Failure("Problem adding photo"); + } +} diff --git a/Application/Features/Photos/Commands/AddPhoto/PhotoUploadResult.cs b/Application/Features/Photos/Commands/AddPhoto/PhotoUploadResult.cs new file mode 100644 index 0000000..441c9bb --- /dev/null +++ b/Application/Features/Photos/Commands/AddPhoto/PhotoUploadResult.cs @@ -0,0 +1,7 @@ +namespace Application.Features.Photos.Commands.AddPhoto; + +public class PhotoUploadResult +{ + public string PublicId { get; set; } + public string Url { get; set; } +} diff --git a/Application/Features/Photos/Commands/DeletePhoto/DeletePhotoCommand.cs b/Application/Features/Photos/Commands/DeletePhoto/DeletePhotoCommand.cs new file mode 100644 index 0000000..cb3b7d9 --- /dev/null +++ b/Application/Features/Photos/Commands/DeletePhoto/DeletePhotoCommand.cs @@ -0,0 +1,9 @@ +using Application.Core; +using MediatR; + +namespace Application.Features.Photos.Commands.DeletePhoto; + +public class DeletePhotoCommand : IRequest> +{ + public string Id { get; set; } +} diff --git a/Application/Features/Photos/Commands/DeletePhoto/DeletePhotoHandler.cs b/Application/Features/Photos/Commands/DeletePhoto/DeletePhotoHandler.cs new file mode 100644 index 0000000..7264220 --- /dev/null +++ b/Application/Features/Photos/Commands/DeletePhoto/DeletePhotoHandler.cs @@ -0,0 +1,35 @@ +using Application.Core; +using Application.Features.Photos.Commands.AddPhoto; +using Application.Interfaces; +using Domain.Entities; +using MediatR; +using Microsoft.EntityFrameworkCore; +using Persistence; + +namespace Application.Features.Photos.Commands.DeletePhoto; + +public class DeletePhotoHandler(DataContext context, IPhotoAccessor photoAccessor, IUserAccessor userAccessor) + : IRequestHandler> +{ + public async Task> Handle(DeletePhotoCommand request, CancellationToken cancellationToken) + { + var user = await context.Users.Include(p => p.Photos) + .FirstOrDefaultAsync(x => x.UserName == userAccessor.GetUsername()); + + var photo = user.Photos.FirstOrDefault(x => x.Id == request.Id); + + if (photo == null) return null; + + if (photo.IsMain) return Result.Failure("You cannot delete your main photo"); + + var result = await photoAccessor.DeletePhoto(photo.Id); + + if (result == null) return Result.Failure("Problem deleting photo"); + + user.Photos.Remove(photo); + + var success = await context.SaveChangesAsync() > 0; + + return success ? Result.Success(Unit.Value) : Result.Failure("Problem deleting photo"); + } +} diff --git a/Application/Features/Photos/Commands/SetMainPhoto/SetMainPhotoCommand.cs b/Application/Features/Photos/Commands/SetMainPhoto/SetMainPhotoCommand.cs new file mode 100644 index 0000000..fa2e5ca --- /dev/null +++ b/Application/Features/Photos/Commands/SetMainPhoto/SetMainPhotoCommand.cs @@ -0,0 +1,9 @@ +using Application.Core; +using MediatR; + +namespace Application.Features.Photos.Commands.SetMainPhoto; + +public class SetMainPhotoCommand : IRequest> +{ + public string Id { get; set; } +} diff --git a/Application/Features/Photos/Commands/SetMainPhoto/SetMainPhotoHandler.cs b/Application/Features/Photos/Commands/SetMainPhoto/SetMainPhotoHandler.cs new file mode 100644 index 0000000..34a1664 --- /dev/null +++ b/Application/Features/Photos/Commands/SetMainPhoto/SetMainPhotoHandler.cs @@ -0,0 +1,31 @@ +using Application.Core; +using Application.Interfaces; +using MediatR; +using Microsoft.EntityFrameworkCore; +using Persistence; + +namespace Application.Features.Photos.Commands.SetMainPhoto; + +public class SetMainPhotoHandler(DataContext context, IUserAccessor userAccessor) + : IRequestHandler> +{ + public async Task> Handle(SetMainPhotoCommand request, CancellationToken cancellationToken) + { + var user = await context.Users + .Include(p => p.Photos) + .FirstOrDefaultAsync(x => x.UserName == userAccessor.GetUsername()); + + var photo = user.Photos.FirstOrDefault(x => x.Id == request.Id); + + if (photo == null) return null; + + var currentMain = user.Photos.FirstOrDefault(x => x.IsMain); + + if (currentMain != null) currentMain.IsMain = false; + + photo.IsMain = true; + var success = await context.SaveChangesAsync() > 0; + + return success ? Result.Success(Unit.Value) : Result.Failure("Problem setting main photo"); + } +} diff --git a/Application/Features/Profiles/Contracts/Profile.cs b/Application/Features/Profiles/Contracts/Profile.cs new file mode 100644 index 0000000..0c92008 --- /dev/null +++ b/Application/Features/Profiles/Contracts/Profile.cs @@ -0,0 +1,12 @@ +using Domain.Entities; + +namespace Application.Features.Profiles.Contracts; + +public class Profile +{ + public string Username { get; set; } + public string DisplayName { get; set; } + public string Bio { get; set; } + public string Image { get; set; } + public ICollection Photos { get; set; } +} diff --git a/Application/Features/Profiles/Queries/GetProfile/GetProfileHandler.cs b/Application/Features/Profiles/Queries/GetProfile/GetProfileHandler.cs new file mode 100644 index 0000000..c9098a0 --- /dev/null +++ b/Application/Features/Profiles/Queries/GetProfile/GetProfileHandler.cs @@ -0,0 +1,21 @@ +using Application.Core; +using AutoMapper; +using AutoMapper.QueryableExtensions; +using MediatR; +using Microsoft.EntityFrameworkCore; +using Persistence; +using Profile = Application.Features.Profiles.Contracts.Profile; + +namespace Application.Features.Profiles.Queries.GetProfile; + +public class GetProfileHandler(DataContext context, IMapper mapper) : IRequestHandler> +{ + public async Task> Handle(GetProfileQuery request, CancellationToken cancellationToken) + { + var user = await context.Users + .ProjectTo(mapper.ConfigurationProvider) + .SingleOrDefaultAsync(x => x.Username == request.Username); + + return Result.Success(user); + } +} diff --git a/Application/Features/Profiles/Queries/GetProfile/GetProfileQuery.cs b/Application/Features/Profiles/Queries/GetProfile/GetProfileQuery.cs new file mode 100644 index 0000000..b56b33b --- /dev/null +++ b/Application/Features/Profiles/Queries/GetProfile/GetProfileQuery.cs @@ -0,0 +1,10 @@ +using Application.Core; +using Application.Features.Profiles.Contracts; +using MediatR; + +namespace Application.Features.Profiles.Queries.GetProfile; + +public class GetProfileQuery : IRequest> +{ + public string Username { get; set; } +} diff --git a/Application/Interfaces/IPhotoAccessor.cs b/Application/Interfaces/IPhotoAccessor.cs new file mode 100644 index 0000000..7f661d7 --- /dev/null +++ b/Application/Interfaces/IPhotoAccessor.cs @@ -0,0 +1,10 @@ +using Application.Features.Photos.Commands.AddPhoto; +using Microsoft.AspNetCore.Http; + +namespace Application.Interfaces; + +public interface IPhotoAccessor +{ + Task AddPhoto(IFormFile file); + Task DeletePhoto(string publicId); +} diff --git a/Domain/Entities/Photo.cs b/Domain/Entities/Photo.cs new file mode 100644 index 0000000..4da33e5 --- /dev/null +++ b/Domain/Entities/Photo.cs @@ -0,0 +1,8 @@ +namespace Domain.Entities; + +public class Photo +{ + public string Id { get; set; } + public string Url { get; set; } + public bool IsMain { get; set; } +} diff --git a/Domain/Entities/User.cs b/Domain/Entities/User.cs index 3bae9c8..b951a93 100644 --- a/Domain/Entities/User.cs +++ b/Domain/Entities/User.cs @@ -7,4 +7,5 @@ public class User : IdentityUser public string DisplayName { get; set; } public string Bio { get; set; } public ICollection Activities { get; set; } + public ICollection Photos { get; set; } } diff --git a/Infrastructure/Infrastructure.csproj b/Infrastructure/Infrastructure.csproj index 7176e42..b01d298 100644 --- a/Infrastructure/Infrastructure.csproj +++ b/Infrastructure/Infrastructure.csproj @@ -4,6 +4,10 @@ + + + + net8.0 enable diff --git a/Infrastructure/Photos/CloudinarySettings.cs b/Infrastructure/Photos/CloudinarySettings.cs new file mode 100644 index 0000000..28c577b --- /dev/null +++ b/Infrastructure/Photos/CloudinarySettings.cs @@ -0,0 +1,8 @@ +namespace Infrastructure.Photos; + +public class CloudinarySettings +{ + public string CloudName { get; set; } + public string ApiKey { get; set; } + public string ApiSecret { get; set; } +} diff --git a/Infrastructure/Photos/PhotoAccessor.cs b/Infrastructure/Photos/PhotoAccessor.cs new file mode 100644 index 0000000..3f6fc01 --- /dev/null +++ b/Infrastructure/Photos/PhotoAccessor.cs @@ -0,0 +1,55 @@ +using Application.Features.Photos.Commands.AddPhoto; +using Application.Interfaces; +using CloudinaryDotNet; +using CloudinaryDotNet.Actions; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Options; + +namespace Infrastructure.Photos; + +public class PhotoAccessor : IPhotoAccessor +{ + private readonly Cloudinary _cloudinary; + + public PhotoAccessor(IOptions config) + { + var account = new Account( + config.Value.CloudName, + config.Value.ApiKey, + config.Value.ApiSecret + ); + _cloudinary = new Cloudinary(account); + } + + public async Task AddPhoto(IFormFile file) + { + if (file.Length <= 0) return null; + + await using var stream = file.OpenReadStream(); + var uploadParams = new ImageUploadParams + { + File = new FileDescription(file.FileName, stream), + Transformation = new Transformation().Height(500).Width(500).Crop("fill") + }; + + var uploadResult = await _cloudinary.UploadAsync(uploadParams); + + if (uploadResult.Error != null) + { + throw new Exception(uploadResult.Error.Message); + } + + return new PhotoUploadResult + { + PublicId = uploadResult.PublicId, + Url = uploadResult.SecureUrl.ToString() + }; + } + + public async Task DeletePhoto(string publicId) + { + var deleteParams = new DeletionParams(publicId); + var result = await _cloudinary.DestroyAsync(deleteParams); + return result.Result == "ok" ? result.Result : null; + } +} diff --git a/Persistence/DataContext.cs b/Persistence/DataContext.cs index cdc733d..fb580c3 100644 --- a/Persistence/DataContext.cs +++ b/Persistence/DataContext.cs @@ -8,6 +8,7 @@ public class DataContext(DbContextOptions options) : IdentityDbContext(opt { public DbSet Activities { get; set; } public DbSet ActivityAttendees { get; set; } + public DbSet Photos { get; set; } protected override void OnModelCreating(ModelBuilder builder) { diff --git a/Persistence/Migrations/20240823124603_PhotoEntityAdded.Designer.cs b/Persistence/Migrations/20240823124603_PhotoEntityAdded.Designer.cs new file mode 100644 index 0000000..2db9f10 --- /dev/null +++ b/Persistence/Migrations/20240823124603_PhotoEntityAdded.Designer.cs @@ -0,0 +1,383 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Persistence; + +#nullable disable + +namespace Persistence.Migrations +{ + [DbContext(typeof(DataContext))] + [Migration("20240823124603_PhotoEntityAdded")] + partial class PhotoEntityAdded + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "8.0.8"); + + modelBuilder.Entity("Domain.Entities.Activity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Category") + .HasColumnType("TEXT"); + + b.Property("City") + .HasColumnType("TEXT"); + + b.Property("Date") + .HasColumnType("TEXT"); + + b.Property("Description") + .HasColumnType("TEXT"); + + b.Property("IsCancelled") + .HasColumnType("INTEGER"); + + b.Property("Title") + .HasColumnType("TEXT"); + + b.Property("Venue") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("Activities"); + }); + + modelBuilder.Entity("Domain.Entities.ActivityAttendee", b => + { + b.Property("ActivityId") + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.Property("IsHost") + .HasColumnType("INTEGER"); + + b.HasKey("ActivityId", "UserId"); + + b.HasIndex("UserId"); + + b.ToTable("ActivityAttendees"); + }); + + modelBuilder.Entity("Domain.Entities.Photo", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("IsMain") + .HasColumnType("INTEGER"); + + b.Property("Url") + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("Photos"); + }); + + modelBuilder.Entity("Domain.Entities.User", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AccessFailedCount") + .HasColumnType("INTEGER"); + + b.Property("Bio") + .HasColumnType("TEXT"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("TEXT"); + + b.Property("DisplayName") + .HasColumnType("TEXT"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("EmailConfirmed") + .HasColumnType("INTEGER"); + + b.Property("LockoutEnabled") + .HasColumnType("INTEGER"); + + b.Property("LockoutEnd") + .HasColumnType("TEXT"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("PasswordHash") + .HasColumnType("TEXT"); + + b.Property("PhoneNumber") + .HasColumnType("TEXT"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("INTEGER"); + + b.Property("SecurityStamp") + .HasColumnType("TEXT"); + + b.Property("TwoFactorEnabled") + .HasColumnType("INTEGER"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasDatabaseName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex"); + + b.ToTable("AspNetUsers", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("TEXT"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("NormalizedName") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasDatabaseName("RoleNameIndex"); + + b.ToTable("AspNetRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ClaimType") + .HasColumnType("TEXT"); + + b.Property("ClaimValue") + .HasColumnType("TEXT"); + + b.Property("RoleId") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ClaimType") + .HasColumnType("TEXT"); + + b.Property("ClaimValue") + .HasColumnType("TEXT"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider") + .HasColumnType("TEXT"); + + b.Property("ProviderKey") + .HasColumnType("TEXT"); + + b.Property("ProviderDisplayName") + .HasColumnType("TEXT"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.Property("UserId") + .HasColumnType("TEXT"); + + b.Property("RoleId") + .HasColumnType("TEXT"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("TEXT"); + + b.Property("LoginProvider") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Value") + .HasColumnType("TEXT"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens", (string)null); + }); + + modelBuilder.Entity("Domain.Entities.ActivityAttendee", b => + { + b.HasOne("Domain.Entities.Activity", "Activity") + .WithMany("Attendees") + .HasForeignKey("ActivityId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Domain.Entities.User", "User") + .WithMany("Activities") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Activity"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.Photo", b => + { + b.HasOne("Domain.Entities.User", null) + .WithMany("Photos") + .HasForeignKey("UserId"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("Domain.Entities.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("Domain.Entities.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Domain.Entities.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("Domain.Entities.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Domain.Entities.Activity", b => + { + b.Navigation("Attendees"); + }); + + modelBuilder.Entity("Domain.Entities.User", b => + { + b.Navigation("Activities"); + + b.Navigation("Photos"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Persistence/Migrations/20240823124603_PhotoEntityAdded.cs b/Persistence/Migrations/20240823124603_PhotoEntityAdded.cs new file mode 100644 index 0000000..84b67dd --- /dev/null +++ b/Persistence/Migrations/20240823124603_PhotoEntityAdded.cs @@ -0,0 +1,45 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Persistence.Migrations +{ + /// + public partial class PhotoEntityAdded : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "Photos", + columns: table => new + { + Id = table.Column(type: "TEXT", nullable: false), + Url = table.Column(type: "TEXT", nullable: true), + IsMain = table.Column(type: "INTEGER", nullable: false), + UserId = table.Column(type: "TEXT", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Photos", x => x.Id); + table.ForeignKey( + name: "FK_Photos_AspNetUsers_UserId", + column: x => x.UserId, + principalTable: "AspNetUsers", + principalColumn: "Id"); + }); + + migrationBuilder.CreateIndex( + name: "IX_Photos_UserId", + table: "Photos", + column: "UserId"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "Photos"); + } + } +} diff --git a/Persistence/Migrations/DataContextModelSnapshot.cs b/Persistence/Migrations/DataContextModelSnapshot.cs index 8c3702f..2ec8dd0 100644 --- a/Persistence/Migrations/DataContextModelSnapshot.cs +++ b/Persistence/Migrations/DataContextModelSnapshot.cs @@ -67,6 +67,27 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("ActivityAttendees"); }); + modelBuilder.Entity("Domain.Entities.Photo", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("IsMain") + .HasColumnType("INTEGER"); + + b.Property("Url") + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("Photos"); + }); + modelBuilder.Entity("Domain.Entities.User", b => { b.Property("Id") @@ -284,6 +305,13 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Navigation("User"); }); + modelBuilder.Entity("Domain.Entities.Photo", b => + { + b.HasOne("Domain.Entities.User", null) + .WithMany("Photos") + .HasForeignKey("UserId"); + }); + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => { b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) @@ -343,6 +371,8 @@ protected override void BuildModel(ModelBuilder modelBuilder) modelBuilder.Entity("Domain.Entities.User", b => { b.Navigation("Activities"); + + b.Navigation("Photos"); }); #pragma warning restore 612, 618 }