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 9a47220..919aad1 100644 Binary files a/API/activity-hub.db and b/API/activity-hub.db differ diff --git a/API/activity-hub.db-shm b/API/activity-hub.db-shm index 43c15b6..1126f28 100644 Binary files a/API/activity-hub.db-shm and b/API/activity-hub.db-shm differ diff --git a/API/activity-hub.db-wal b/API/activity-hub.db-wal index c30741a..1240b4a 100644 Binary files a/API/activity-hub.db-wal and b/API/activity-hub.db-wal differ 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 }