Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

AH-16 API image upload #23

Merged
merged 1 commit into from
Aug 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions API/Controllers/AccountController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ public class AccountController(UserManager<User> userManager, TokenService token
[HttpPost("login")]
public async Task<ActionResult<AuthUserDto>> 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();

Expand Down Expand Up @@ -65,7 +65,7 @@ public async Task<ActionResult<AuthUserDto>> Register(RegisterDto registerDto)
[HttpGet]
public async Task<ActionResult<AuthUserDto>> 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);
}
Expand All @@ -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
};
Expand Down
27 changes: 27 additions & 0 deletions API/Controllers/PhotosController.cs
Original file line number Diff line number Diff line change
@@ -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<IActionResult> Add([FromForm] AddPhotoCommand command)
{
return HandleResult(await Mediator.Send(command));
}

[HttpDelete("{id}")]
public async Task<IActionResult> Delete(string id)
{
return HandleResult(await Mediator.Send(new DeletePhotoCommand { Id = id }));
}

[HttpPost("{id}/setMain")]
public async Task<IActionResult> SetMain(string id)
{
return HandleResult(await Mediator.Send(new SetMainPhotoCommand { Id = id }));
}
}
13 changes: 13 additions & 0 deletions API/Controllers/ProfilesController.cs
Original file line number Diff line number Diff line change
@@ -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<IActionResult> GetProfile(string username)
{
return HandleResult(await Mediator.Send(new GetProfileQuery { Username = username }));
}
}
3 changes: 3 additions & 0 deletions API/Extensions/ApplicationServiceExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using Application.Interfaces;
using FluentValidation;
using FluentValidation.AspNetCore;
using Infrastructure.Photos;
using Infrastructure.Security;
using Microsoft.EntityFrameworkCore;
using Persistence;
Expand All @@ -26,6 +27,8 @@ public static IServiceCollection AddApplicationServices(this IServiceCollection
services.AddValidatorsFromAssemblyContaining<CreateActivityValidator>();
services.AddHttpContextAccessor();
services.AddScoped<IUserAccessor, UserAccessor>();
services.AddScoped<IPhotoAccessor, PhotoAccessor>();
services.Configure<CloudinarySettings>(config.GetSection("Cloudinary"));

return services;
}
Expand Down
Binary file modified API/activity-hub.db
Binary file not shown.
Binary file modified API/activity-hub.db-shm
Binary file not shown.
Binary file modified API/activity-hub.db-wal
Binary file not shown.
3 changes: 3 additions & 0 deletions Application/Application.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@

<ItemGroup>
<Folder Include="Features\Attendance\Queries\"/>
<Folder Include="Features\Photos\Queries\"/>
<Folder Include="Features\Profiles\Commands\"/>
<Folder Include="Photos\"/>
</ItemGroup>

<PropertyGroup>
Expand Down
9 changes: 7 additions & 2 deletions Application/Core/MappingProfiles.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using Application.Features.Activities.Contracts;
using Application.Features.Attendance.Contracts;
using AutoMapper;
using Domain.Entities;

Expand All @@ -14,9 +15,13 @@ public MappingProfiles()
.ForMember(d => d.HostUsername, o => o.MapFrom(s => s.Attendees
.FirstOrDefault(x => x.IsHost).User.UserName));

CreateMap<ActivityAttendee, Profiles.Profile>()
CreateMap<ActivityAttendee, AttendeeDto>()
.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<User, Features.Profiles.Contracts.Profile>()
.ForMember(d => d.Image, s => s.MapFrom(o => o.Photos.FirstOrDefault(x => x.IsMain).Url));
}
}
6 changes: 4 additions & 2 deletions Application/Features/Activities/Contracts/ActivityDto.cs
Original file line number Diff line number Diff line change
@@ -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;

Expand All @@ -13,5 +15,5 @@ public class ActivityDto
public string Venue { get; set; }
public string HostUsername { get; set; }
public bool IsCancelled { get; set; }
public ICollection<Profile> Attendees { get; set; }
public ICollection<AttendeeDto> Attendees { get; set; }
}
Original file line number Diff line number Diff line change
@@ -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; }
Expand Down
11 changes: 11 additions & 0 deletions Application/Features/Photos/Commands/AddPhoto/AddPhotoCommand.cs
Original file line number Diff line number Diff line change
@@ -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<Result<Photo>>
{
public IFormFile File { get; set; }
}
36 changes: 36 additions & 0 deletions Application/Features/Photos/Commands/AddPhoto/AddPhotoHandler.cs
Original file line number Diff line number Diff line change
@@ -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<AddPhotoCommand, Result<Photo>>
{
public async Task<Result<Photo>> 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<Photo>.Success(photo) : Result<Photo>.Failure("Problem adding photo");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
namespace Application.Features.Photos.Commands.AddPhoto;

public class PhotoUploadResult
{
public string PublicId { get; set; }
public string Url { get; set; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
using Application.Core;
using MediatR;

namespace Application.Features.Photos.Commands.DeletePhoto;

public class DeletePhotoCommand : IRequest<Result<Unit>>
{
public string Id { get; set; }
}
Original file line number Diff line number Diff line change
@@ -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<DeletePhotoCommand, Result<Unit>>
{
public async Task<Result<Unit>> 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<Unit>.Failure("You cannot delete your main photo");

var result = await photoAccessor.DeletePhoto(photo.Id);

if (result == null) return Result<Unit>.Failure("Problem deleting photo");

user.Photos.Remove(photo);

var success = await context.SaveChangesAsync() > 0;

return success ? Result<Unit>.Success(Unit.Value) : Result<Unit>.Failure("Problem deleting photo");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
using Application.Core;
using MediatR;

namespace Application.Features.Photos.Commands.SetMainPhoto;

public class SetMainPhotoCommand : IRequest<Result<Unit>>
{
public string Id { get; set; }
}
Original file line number Diff line number Diff line change
@@ -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<SetMainPhotoCommand, Result<Unit>>
{
public async Task<Result<Unit>> 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<Unit>.Success(Unit.Value) : Result<Unit>.Failure("Problem setting main photo");
}
}
12 changes: 12 additions & 0 deletions Application/Features/Profiles/Contracts/Profile.cs
Original file line number Diff line number Diff line change
@@ -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<Photo> Photos { get; set; }
}
Original file line number Diff line number Diff line change
@@ -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<GetProfileQuery, Result<Profile>>
{
public async Task<Result<Profile>> Handle(GetProfileQuery request, CancellationToken cancellationToken)
{
var user = await context.Users
.ProjectTo<Profile>(mapper.ConfigurationProvider)
.SingleOrDefaultAsync(x => x.Username == request.Username);

return Result<Profile>.Success(user);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
using Application.Core;
using Application.Features.Profiles.Contracts;
using MediatR;

namespace Application.Features.Profiles.Queries.GetProfile;

public class GetProfileQuery : IRequest<Result<Profile>>
{
public string Username { get; set; }
}
10 changes: 10 additions & 0 deletions Application/Interfaces/IPhotoAccessor.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
using Application.Features.Photos.Commands.AddPhoto;
using Microsoft.AspNetCore.Http;

namespace Application.Interfaces;

public interface IPhotoAccessor
{
Task<PhotoUploadResult> AddPhoto(IFormFile file);
Task<string> DeletePhoto(string publicId);
}
8 changes: 8 additions & 0 deletions Domain/Entities/Photo.cs
Original file line number Diff line number Diff line change
@@ -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; }
}
1 change: 1 addition & 0 deletions Domain/Entities/User.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,5 @@ public class User : IdentityUser
public string DisplayName { get; set; }
public string Bio { get; set; }
public ICollection<ActivityAttendee> Activities { get; set; }
public ICollection<Photo> Photos { get; set; }
}
4 changes: 4 additions & 0 deletions Infrastructure/Infrastructure.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@
<ProjectReference Include="..\Application\Application.csproj"/>
</ItemGroup>

<ItemGroup>
<PackageReference Include="CloudinaryDotNet" Version="1.26.2"/>
</ItemGroup>

<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
Expand Down
8 changes: 8 additions & 0 deletions Infrastructure/Photos/CloudinarySettings.cs
Original file line number Diff line number Diff line change
@@ -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; }
}
Loading