diff --git a/AutoMapperProfile.cs b/AutoMapperProfile.cs index b875bfc01321c0b950aed94bc47d69b1bcfbbb55..dfec55e8dccd2f861d6b63cf9530399b8c7c0aea 100644 --- a/AutoMapperProfile.cs +++ b/AutoMapperProfile.cs @@ -1,16 +1,16 @@ -using Group17profile.Models.DTOs; -using Group17profile.Models.Entities; +namespace Group17profile; -namespace Group17profile; +using Models.DTOs; +using Models.Entities; +using Profile = AutoMapper.Profile; -public class AutoMapperProfile : AutoMapper.Profile +public class AutoMapperProfile : Profile { public AutoMapperProfile() { CreateMap<User, UserDTO>(); CreateMap<UserDTO, User>(); - CreateMap<Profile, ProfileDTO>().ForMember(opt => opt.FavouriteShows, src => src.Ignore()); - CreateMap<ProfileDTO, Profile>().ForMember(opt => opt.FavouriteShows, src => src.Ignore()); - + CreateMap<Models.Entities.Profile, ProfileDTO>().ForMember(opt => opt.FavouriteShows, src => src.Ignore()); + CreateMap<ProfileDTO, Models.Entities.Profile>().ForMember(opt => opt.FavouriteShows, src => src.Ignore()); } } \ No newline at end of file diff --git a/Constants.cs b/Constants.cs index 5f08ef3f800f197c94f240b51ae2afd663c14295..76bfeffc2dfb729690df6defb13280c0ff65e8e5 100644 --- a/Constants.cs +++ b/Constants.cs @@ -19,6 +19,11 @@ public class Constants public const string ProfileSasToken = "profileSasToken"; } + public static class AzureBlobContainer + { + public const string BannerPictures = "banner-pictures"; + } + public static class AuthorizationPolicies { public const string HasUserId = "HasUserId"; diff --git a/Controllers/DefaultController.cs b/Controllers/DefaultController.cs index e19bfea73537538b6dbbfddacfdc01cae309996a..2dce994645a389916717549523f6f23d2b60005d 100644 --- a/Controllers/DefaultController.cs +++ b/Controllers/DefaultController.cs @@ -1,8 +1,8 @@ -using System.Reflection; -using Group17profile.Middleware; -using Microsoft.AspNetCore.Mvc; +namespace Group17profile.Controllers; -namespace Group17profile.Controllers; +using System.Reflection; +using Microsoft.AspNetCore.Mvc; +using Middleware; public class DefaultController : Controller { diff --git a/Controllers/DefaultProfileController.cs b/Controllers/DefaultProfileController.cs index d681747f905f19e40e4e867f4e4aa76fcd9307c5..ec3012e789294ebf9a3079122fe296cf58a477c9 100644 --- a/Controllers/DefaultProfileController.cs +++ b/Controllers/DefaultProfileController.cs @@ -1,7 +1,6 @@ -using System.Security.Claims; -using Microsoft.VisualBasic; +namespace Group17profile.Controllers; -namespace Group17profile.Controllers; +using System.Security.Claims; public class DefaultProfileController : DefaultController { diff --git a/Controllers/ProfileController.cs b/Controllers/ProfileController.cs index 56bb2e23df1c8422e955bbbe69074e6ccf41bcb7..b4c8898c11f0cdae47ab3644041ec03b11e084c5 100644 --- a/Controllers/ProfileController.cs +++ b/Controllers/ProfileController.cs @@ -1,14 +1,11 @@ -using Group17profile.helpers; -using Group17profile.Middleware; -using Group17profile.Models.DTOs; -using Group17profile.Models.Entities; -using Group17profile.Services; -using Microsoft.AspNetCore.Authorization; +namespace Group17profile.Controllers; + using Microsoft.AspNetCore.Mvc; +using Middleware; +using Models.DTOs; +using Services; using Swashbuckle.AspNetCore.Annotations; -namespace Group17profile.Controllers; - [Route("api/[controller]")] [ApiController] public class ProfileController : DefaultProfileController @@ -20,6 +17,21 @@ public class ProfileController : DefaultProfileController _profileService = profileService; } + [HttpGet("GetProfileForUser")] + [SwaggerResponse(200, Type = typeof(ResponseEnvelope<UserProfileDTO>))] + [SwaggerResponse(400, Type = typeof(ResponseEnvelope<BadRequestObjectResult>))] + public async Task<ActionResult<ResponseEnvelope<UserProfileDTO>>> GetProfileForUser() + { + try + { + return Ok(await _profileService.GetProfileForUser(1)); + } + catch (Exception ex) + { + return BadRequest(ex.Message); + } + } + [HttpPost("CreateOrUpdateProfile")] [SwaggerResponse(200, Type = typeof(ResponseEnvelope<ProfileDTO>))] [SwaggerResponse(400, Type = typeof(ResponseEnvelope<BadRequestObjectResult>))] @@ -27,14 +39,21 @@ public class ProfileController : DefaultProfileController { try { - var newProfile = await _profileService.CreateOrUpdateProfile(profile, UserId); - + var newProfile = await _profileService.CreateOrUpdateProfile(profile, 1); return Ok(newProfile); } catch (Exception ex) { return BadRequest(ex.Message); } + } + [HttpPost("UploadBannerPicture")] + [Consumes("multipart/form-data")] + public async Task<ActionResult<ResponseEnvelope<string>>> UploadBannerPicture(IFormFile? profilePicture) + { + if (profilePicture == null) + return BadRequest("Please upload a file."); + return Ok(await _profileService.UploadBannerPicture(1, profilePicture)); } } \ No newline at end of file diff --git a/Group17profile.csproj b/Group17profile.csproj index dfa48742a64a224285cc44b3d7b1194dfad7d7e9..f2bec7eb2818634fe9296d16926fbfae9493e6f3 100644 --- a/Group17profile.csproj +++ b/Group17profile.csproj @@ -1,33 +1,32 @@ <Project Sdk="Microsoft.NET.Sdk.Web"> - <PropertyGroup> - <TargetFramework>net8.0</TargetFramework> - <Nullable>enable</Nullable> - <ImplicitUsings>enable</ImplicitUsings> - </PropertyGroup> + <PropertyGroup> + <TargetFramework>net8.0</TargetFramework> + <Nullable>enable</Nullable> + <ImplicitUsings>enable</ImplicitUsings> + </PropertyGroup> - <ItemGroup> - <PackageReference Include="AuthorizeNet" Version="2.0.3" /> - <PackageReference Include="AutoMapper" Version="12.0.1" /> - <PackageReference Include="Microsoft.AspNetCore.Authorization" Version="8.0.0-preview.2.23153.2" /> - <PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="7.0.4" /> - <PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.0-preview.2.23128.3" /> - <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.0-preview.2.23128.3"> - <PrivateAssets>all</PrivateAssets> - <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> - </PackageReference> - <PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="8.0.0-preview.2.23128.3" /> - <PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="8.0.0-preview.2.23128.3"> - <PrivateAssets>all</PrivateAssets> - <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> - </PackageReference> - <PackageReference Include="Newtonsoft.Json" Version="13.0.3" /> - <PackageReference Include="Swashbuckle.AspNetCore" Version="6.4.0" /> - <PackageReference Include="Swashbuckle.AspNetCore.Annotations" Version="6.5.0" /> - </ItemGroup> - - <ItemGroup> - <Folder Include="Settings" /> - </ItemGroup> + <ItemGroup> + <PackageReference Include="AuthorizeNet" Version="2.0.3"/> + <PackageReference Include="AutoMapper" Version="12.0.1"/> + <PackageReference Include="Azure.Storage.Blobs" Version="12.16.0-beta.1"/> + <PackageReference Include="Azure.Storage.Common" Version="12.15.0-beta.1"/> + <PackageReference Include="Microsoft.AspNetCore.Authorization" Version="8.0.0-preview.2.23153.2"/> + <PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="7.0.4"/> + <PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.0-preview.2.23128.3"/> + <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.0-preview.2.23128.3"> + <PrivateAssets>all</PrivateAssets> + <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> + </PackageReference> + <PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="8.0.0-preview.2.23128.3"/> + <PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="8.0.0-preview.2.23128.3"> + <PrivateAssets>all</PrivateAssets> + <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> + </PackageReference> + <PackageReference Include="Newtonsoft.Json" Version="13.0.3"/> + <PackageReference Include="SixLabors.ImageSharp" Version="3.0.1"/> + <PackageReference Include="Swashbuckle.AspNetCore" Version="6.4.0"/> + <PackageReference Include="Swashbuckle.AspNetCore.Annotations" Version="6.5.0"/> + </ItemGroup> </Project> diff --git a/Helpers/ResponseOperationFilter.cs b/Helpers/ResponseOperationFilter.cs index 40ee5d6480880125e3d5d1c45791218dfb932305..5ef82cedf2030e46f4618ea5d36c33773c641114 100644 --- a/Helpers/ResponseOperationFilter.cs +++ b/Helpers/ResponseOperationFilter.cs @@ -1,11 +1,5 @@ -using System.Reflection; -using Microsoft.AspNetCore.Authorization; -using Microsoft.OpenApi.Models; -using Swashbuckle.AspNetCore.SwaggerGen; - -namespace Group17profile.helpers; +namespace Group17profile.helpers; public class ResponseOperationFilter { - } \ No newline at end of file diff --git a/Middleware/ExceptionMiddleware.cs b/Middleware/ExceptionMiddleware.cs index e13ec9dd42aa7ff4b2c9cf6f2c08f25b1e4161b0..76f8fe5d3591918df4cebb9573e6ebff19e943af 100644 --- a/Middleware/ExceptionMiddleware.cs +++ b/Middleware/ExceptionMiddleware.cs @@ -1,9 +1,9 @@ -using System.Net; -using System.Security.Authentication; -using Group17profile.Exceptions; -using Newtonsoft.Json; +namespace Group17profile.Middleware; -namespace Group17profile.Middleware; +using System.Net; +using Exceptions; +using Microsoft.EntityFrameworkCore; +using Newtonsoft.Json; public class ExceptionMiddleware { @@ -49,7 +49,7 @@ public class ExceptionMiddleware logger.LogWarning(ex.Message); break; - case Microsoft.EntityFrameworkCore.DbUpdateException dbex: + case DbUpdateException dbex: envelope = new ResponseEnvelope<object>(dbex, userMessage: "Database Error has occurred. Please try again or report the issue."); context.Response.StatusCode = (int) HttpStatusCode.InternalServerError; @@ -86,8 +86,6 @@ public class ResponseEnvelope public class ResponseEnvelope<T> : ResponseEnvelope { - public new T? Data { get; set; } - public ResponseEnvelope() { } @@ -95,7 +93,7 @@ public class ResponseEnvelope<T> : ResponseEnvelope public ResponseEnvelope(Exception ex, int statusCode = 400, string? userMessage = null) : this( new List<ErrorDetails> { - new ErrorDetails + new() { Message = ex.Message, InnerMessage = ex.InnerException?.Message, @@ -126,6 +124,8 @@ public class ResponseEnvelope<T> : ResponseEnvelope StatusCode = statusCode; } + public new T? Data { get; set; } + public override string ToString() { return JsonConvert.SerializeObject(this); diff --git a/Middleware/SwaggerProfileMiddleware.cs b/Middleware/SwaggerProfileMiddleware.cs index 79ed1a43e79c4de33fdb5b900be66858ead5c8f1..ed687a58aa2fb4d158bb20ff23b5b4da0bb44bf3 100644 --- a/Middleware/SwaggerProfileMiddleware.cs +++ b/Middleware/SwaggerProfileMiddleware.cs @@ -1,7 +1,7 @@ -using System.Net; -using System.Text; +namespace Group17profile.Middleware; -namespace Group17profile.Middleware; +using System.Net; +using System.Text; public class SwaggerProfileMiddleware { @@ -9,7 +9,7 @@ public class SwaggerProfileMiddleware public SwaggerProfileMiddleware(RequestDelegate next) { - this._next = next; + _next = next; } public async Task InvokeAsync(HttpContext context) diff --git a/Models/DTOs/ProfileDTO.cs b/Models/DTOs/ProfileDTO.cs index a6727f04d43a94f166c9282015fbdcfb9f257ba5..ff8dfecbfd9e64fb310d3a7992a06cc9d57ddd7e 100644 --- a/Models/DTOs/ProfileDTO.cs +++ b/Models/DTOs/ProfileDTO.cs @@ -1,24 +1,18 @@ -using System.ComponentModel.DataAnnotations; -using Group17profile.Models.DefaultObjects; +namespace Group17profile.Models.DTOs; -namespace Group17profile.Models.DTOs; +using DefaultObjects; public class ProfileDTO : DefaultGuidDTO { public string? Biography { get; set; } - - public string? BannerUrl { get; set; } - - public string? Gender { get; set; } - - public DateTimeOffset? DoB { get; set; } - - public List<string>? FavouriteShows { get; set; } - - public string? Pronoun { get; set; } -} + public string? BannerUrl { get; set; } + public string? Gender { get; set; } + public DateTimeOffset? DoB { get; set; } + public List<string>? FavouriteShows { get; set; } + public string? Pronoun { get; set; } +} \ No newline at end of file diff --git a/Models/DTOs/UserDTO.cs b/Models/DTOs/UserDTO.cs index bca9073777296ad0134dd2971e8b92ed90e272bb..d8e2941ac9e277b51137f9dc8e0bf63eb7eccb22 100644 --- a/Models/DTOs/UserDTO.cs +++ b/Models/DTOs/UserDTO.cs @@ -1,7 +1,7 @@ -using System.ComponentModel.DataAnnotations; -using Group17profile.Models.DefaultObjects; +namespace Group17profile.Models.DTOs; -namespace Group17profile.Models.DTOs; +using System.ComponentModel.DataAnnotations; +using DefaultObjects; public class UserDTO : DefaultIntDTO { @@ -25,7 +25,7 @@ public class UserProfileDTO { public UserDTO? User { get; set; } public ProfileDTO? Profile { get; set; } -} +} public class RefreshTokenRequestDTO { diff --git a/Models/DefaultObjects/DefaultDTO.cs b/Models/DefaultObjects/DefaultDTO.cs index 119ed79c3f429eeb08ababd198f2ed9a130eaf44..e00c64b223d9910026caa44a8c8ed926e932e934 100644 --- a/Models/DefaultObjects/DefaultDTO.cs +++ b/Models/DefaultObjects/DefaultDTO.cs @@ -18,4 +18,4 @@ public class DefaultIntDTO } public int Id { get; set; } -} +} \ No newline at end of file diff --git a/Models/DefaultObjects/DefaultEntity.cs b/Models/DefaultObjects/DefaultEntity.cs index 1841717198836550dd25c1ff156c16c2f736638c..eed48b4124db49c9a20a64a166105d4155a9df8f 100644 --- a/Models/DefaultObjects/DefaultEntity.cs +++ b/Models/DefaultObjects/DefaultEntity.cs @@ -1,29 +1,29 @@ -using System.ComponentModel.DataAnnotations; -using System.ComponentModel.DataAnnotations.Schema; - -namespace Group17profile.Models.DefaultObjects; +namespace Group17profile.Models.DefaultObjects; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; public class DefaultGuidEntity : IDefaultEntity, IGuidId, ITrackable { + public DateTimeOffset CreatedAt { get; set; } = DateTimeOffset.UtcNow; + + public DateTimeOffset? DeletedAt { get; set; } + [Key] [DatabaseGenerated(DatabaseGeneratedOption.Identity)] public Guid Id { get; set; } - public DateTimeOffset CreatedAt { get; set; } = DateTimeOffset.UtcNow; - - public DateTimeOffset? DeletedAt { get; set; } - public DateTimeOffset? UpdatedAt { get; set; } } - public class DefaultIntEntity : IDefaultEntity, IIntId, ITrackable { public DateTimeOffset CreatedAt { get; set; } = DateTimeOffset.UtcNow; public DateTimeOffset? DeletedAt { get; set; } + [Key] public int Id { get; set; } + public DateTimeOffset? UpdatedAt { get; set; } } diff --git a/Models/Entities/Profile.cs b/Models/Entities/Profile.cs index 38232d17251a6265d3ac6d0fa87e7dc5fb0c4b36..7dfbad30e12229291e29327ebc461487cbb44b6a 100644 --- a/Models/Entities/Profile.cs +++ b/Models/Entities/Profile.cs @@ -1,19 +1,18 @@ -using System.Runtime.InteropServices.JavaScript; -using Group17profile.Models.DefaultObjects; +namespace Group17profile.Models.Entities; -namespace Group17profile.Models.Entities; +using DefaultObjects; public class Profile : DefaultGuidEntity { public string? Biography { get; set; } - + public string? BannerUrl { get; set; } - + public string? Gender { get; set; } - + public DateTimeOffset? DoB { get; set; } - + public string? FavouriteShows { get; set; } - + public string? Pronoun { get; set; } } \ No newline at end of file diff --git a/Models/Entities/User.cs b/Models/Entities/User.cs index e7fdb17f413674c9eb135559a6142f4ee33a6f9c..586be2426a82aee41825a098ce1a54d9676c7999 100644 --- a/Models/Entities/User.cs +++ b/Models/Entities/User.cs @@ -28,6 +28,6 @@ public class User : DefaultIntEntity [ForeignKey("ProfileId")] public virtual Profile? Profile { get; set; } - + [NotMapped] public string Username => FirstName + " " + Surname; } \ No newline at end of file diff --git a/Models/ProfileDbContext.cs b/Models/ProfileDbContext.cs index bb87b984fbb7dd61f5c8e4f44560c21aba599279..2af10fcade4260f55d13eb34d8026f55bb5102fb 100644 --- a/Models/ProfileDbContext.cs +++ b/Models/ProfileDbContext.cs @@ -1,16 +1,19 @@ -using System.ComponentModel.DataAnnotations.Schema; -using Group17profile.Models.DefaultObjects; -using Group17profile.Models.Entities; -using Microsoft.EntityFrameworkCore; +namespace Group17profile.Models; -namespace Group17profile.Models; +using System.ComponentModel.DataAnnotations.Schema; +using DefaultObjects; +using Entities; +using Microsoft.EntityFrameworkCore; public class ProfileDbContext : DbContext { public static string ConnectionStringName = ""; - - public ProfileDbContext(DbContextOptions<ProfileDbContext> options) : base(options){} + + public ProfileDbContext(DbContextOptions<ProfileDbContext> options) : base(options) + { + } + public DbSet<Profile> Profile { get; set; } = null!; public DbSet<User> User { get; set; } = null!; @@ -33,9 +36,8 @@ public class ProfileDbContext : DbContext p.CustomAttributes.Any(a => a.AttributeType == typeof(DatabaseGeneratedAttribute)))) modelBuilder.Entity(entity.ClrType).Property(property.Name).HasDefaultValueSql("SYSDATETIMEOFFSET()"); } - } - + public override int SaveChanges(bool acceptAllChangesOnSuccess) { UpdateTimestamps(); diff --git a/Program.cs b/Program.cs index 078397e00e375cd51edfbb8f2a813beff1d52891..3e1a4af0f5078a53f4cd43e22112b3e16b9fdbc3 100644 --- a/Program.cs +++ b/Program.cs @@ -1,6 +1,9 @@ using AutoMapper; using Group17profile; using Group17profile.Models; +using Group17profile.Repositories; +using Group17profile.Services; +using Group17profile.Settings; using Microsoft.EntityFrameworkCore; var builder = WebApplication.CreateBuilder(args); @@ -10,7 +13,8 @@ var builder = WebApplication.CreateBuilder(args); builder.Services.AddControllers(); builder.Configuration.AddJsonFile("appsettings.Development.json", true, true); builder.Services.AddDbContext<ProfileDbContext>( - opt => opt.UseSqlServer(builder.Configuration.GetConnectionString("ProfileDb"))); + opt => opt.UseSqlServer(builder.Configuration.GetConnectionString("DbConnectionString"))); +builder.Services.Configure<ConnectionStrings>(builder.Configuration.GetSection("ConnectionStrings")); // Automapper var mapperConfig = new MapperConfiguration(mc => { mc.AddProfile(new AutoMapperProfile()); }); @@ -21,6 +25,13 @@ builder.Services.AddSingleton(mapper); builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(); +// Add repositories in /Repositories +builder.Services.AddTransient(typeof(IBaseRepository<>), typeof(BaseRepository<>)); + +// Add services in /Services +builder.Services.AddTransient<IProfileService, ProfileService>(); +builder.Services.AddTransient<IStorageService, StorageService>(); + var app = builder.Build(); // Configure the HTTP request pipeline. @@ -36,4 +47,4 @@ app.UseAuthorization(); app.MapControllers(); -app.Run(); +app.Run(); \ No newline at end of file diff --git a/Services/ProfileService.cs b/Services/ProfileService.cs index a75cde9cbf41e55f8bad8ed54445ef180d2d3877..04b676604546887a8c2b724d672ac91fb266698b 100644 --- a/Services/ProfileService.cs +++ b/Services/ProfileService.cs @@ -1,51 +1,49 @@ -using System.Text; +namespace Group17profile.Services; + +using System.Text; using AutoMapper; -using Group17profile.Exceptions; -using Group17profile.Models.DTOs; -using Group17profile.Models.Entities; -using Group17profile.Repositories; +using Exceptions; using Microsoft.EntityFrameworkCore; -using Profile = Group17profile.Models.Entities.Profile; - -namespace Group17profile.Services; +using Models.DTOs; +using Models.Entities; +using Repositories; +using Profile = Models.Entities.Profile; public interface IProfileService { Task<ProfileDTO> CreateOrUpdateProfile(ProfileDTO profile, int userId); Task<UserProfileDTO> GetProfileForUser(int userId); Task<string> UploadBannerPicture(int userId, IFormFile bannerPicture); - Task<string> EditBannerPicture(int userId, IFormFile? bannerPicture = null); } public class ProfileService : IProfileService { private readonly IMapper _mapper; private readonly IBaseRepository<Profile> _profileRepository; + private readonly IStorageService _storageService; private readonly IBaseRepository<User> _userRepository; public ProfileService(IMapper mapper, IBaseRepository<Profile> profileRepository, - IBaseRepository<User> userRepository) + IBaseRepository<User> userRepository, IStorageService storageService) { _mapper = mapper; _profileRepository = profileRepository; _userRepository = userRepository; - + _storageService = storageService; } public async Task<ProfileDTO> CreateOrUpdateProfile(ProfileDTO profile, int userId) { var user = _userRepository.GetByIdThrowIfNull(userId); var age = DateTimeOffset.Now.Year - profile.DoB.GetValueOrDefault().Year; - + if (age is <= 16 or >= 100) - { - throw new ProfileException("Please enter valid age"); - } + throw new ProfileException("Please enter valid age."); var newDetails = _mapper.Map<Profile>(profile); var record = _mapper.Map<Profile>(profile); - if (newDetails.FavouriteShows.Any()) + if (!string.IsNullOrWhiteSpace(newDetails.FavouriteShows)) { var builder = new StringBuilder(); foreach (var show in newDetails.FavouriteShows) @@ -53,7 +51,7 @@ public class ProfileService : IProfileService record.FavouriteShows = builder.ToString(); } - + if (user.ProfileId == null) { var newProfile = await _profileRepository.CreateAndSaveAsync(record); @@ -68,21 +66,41 @@ public class ProfileService : IProfileService { var user = _userRepository.GetByIdThrowIfNull(userId); if (user.ProfileId == null) - throw new ProfileException("Failed to retrieve profile for user"); - - var profile = _mapper.Map<ProfileDTO>(_profileRepository.GetAll().AsNoTracking() - .FirstOrDefault(p => p.Id == user.ProfileId)); - return new UserProfileDTO() {User = _mapper.Map<UserDTO>(user), Profile = profile}; - } - - public Task<string> UploadBannerPicture(int userId, IFormFile bannerPicture) - { - throw new NotImplementedException(); + throw new ProfileException("Failed to retrieve profile for user."); + + var profile = await _profileRepository.GetAll().AsNoTracking().FirstOrDefaultAsync(p => p.Id == user.ProfileId); + var record = _mapper.Map<ProfileDTO>(profile); + if (string.IsNullOrWhiteSpace(profile?.FavouriteShows)) + return new UserProfileDTO {User = _mapper.Map<UserDTO>(user), Profile = record}; + var s = profile.FavouriteShows.Split(","); + record.FavouriteShows = s.ToList(); + + return new UserProfileDTO {User = _mapper.Map<UserDTO>(user), Profile = record}; } - - public Task<string> EditBannerPicture(int userId, IFormFile? bannerPicture = null) + + public async Task<string> UploadBannerPicture(int userId, IFormFile bannerPicture) { - throw new NotImplementedException(); + var user = _userRepository.GetByIdThrowIfNull(userId); + + using var ms = new MemoryStream(); + await bannerPicture.CopyToAsync(ms); + ms.Position = 0; + var fileName = $"{user.Id}/{user.FirstName}banner"; + var image = await _storageService.SaveImageAsJpgBlob(Constants.AzureBlobContainer.BannerPictures, fileName, ms); + + var profile = await _profileRepository.GetAll().AsNoTracking().FirstOrDefaultAsync(p => p.Id == user.ProfileId); + if (profile == null) + { + var newProfile = new Profile + { + BannerUrl = image.ToString() + }; + await _profileRepository.CreateAndSaveAsync(newProfile); + return _storageService.GetSasForFile(Constants.AzureBlobContainer.BannerPictures, image.ToString()); + } + + profile.BannerUrl = image.ToString(); + await _profileRepository.UpdateAndSaveAsync(profile); + return _storageService.GetSasForFile(Constants.AzureBlobContainer.BannerPictures, image.ToString()); } - } \ No newline at end of file diff --git a/Services/StorageService.cs b/Services/StorageService.cs new file mode 100644 index 0000000000000000000000000000000000000000..3386e33f99c91524a67ffb5d2761adb7623c1897 --- /dev/null +++ b/Services/StorageService.cs @@ -0,0 +1,69 @@ +namespace Group17profile.Services; + +using System.Web; +using Azure.Storage.Blobs; +using Azure.Storage.Sas; +using Microsoft.Extensions.Options; +using Settings; + +public interface IStorageService +{ + string GetSasForFile(string containerName, string url, DateTimeOffset? expires = null); + + Task<Uri> SaveImageAsJpgBlob(string containerName, string fileName, Stream input, + Dictionary<string, string>? metadata = null); +} + +public class StorageService : IStorageService +{ + private readonly ConnectionStrings _connectionStrings; + + public StorageService(IOptions<ConnectionStrings> connectionStrings) + { + _connectionStrings = connectionStrings.Value; + } + + public string GetSasForFile(string containerName, string url, DateTimeOffset? expires = null) + { + url = GetFileFromUrl(url, containerName) ?? string.Empty; + if (string.IsNullOrWhiteSpace(url)) + return string.Empty; + var container = new BlobContainerClient(_connectionStrings.AzureBlobStorage, containerName); + var blob = container.GetBlobClient(url); + var urlWithSas = blob.GenerateSasUri(BlobSasPermissions.Read, DateTimeOffset.Now.AddHours(3)).ToString(); + return urlWithSas; + } + + public async Task<Uri> SaveImageAsJpgBlob(string containerName, string fileName, Stream input, + Dictionary<string, string>? metadata = null) + { + using var image = await Image.LoadAsync(input); + using var croppedImage = new MemoryStream(); + var clone = image.Clone(context => + context.Resize(new ResizeOptions {Mode = ResizeMode.Max, Size = new Size(1920, 1080)})); + await clone.SaveAsJpegAsync(croppedImage); + croppedImage.Position = 0; + var blob = await GetBlobReference(containerName, fileName); + await blob.UploadAsync(croppedImage); + return blob.Uri; + } + + private async Task<BlobClient> GetBlobReference(string? containerName, string blobName) + { + var container = new BlobContainerClient(_connectionStrings.AzureBlobStorage, containerName); + await container.CreateIfNotExistsAsync(); + var blob = container.GetBlobClient(blobName); + return blob; + } + + private static string? GetFileFromUrl(string url, string container) + { + if (string.IsNullOrWhiteSpace(url)) + return null; + + var userDataContainerString = container + "/"; + var fileUrl = + url[(url.IndexOf(userDataContainerString, StringComparison.Ordinal) + userDataContainerString.Length)..]; + return HttpUtility.UrlDecode(fileUrl); + } +} \ No newline at end of file diff --git a/Settings/ConnectionStrings.cs b/Settings/ConnectionStrings.cs new file mode 100644 index 0000000000000000000000000000000000000000..0a688ac6e4a337a01bb7300e520dce122af61c75 --- /dev/null +++ b/Settings/ConnectionStrings.cs @@ -0,0 +1,7 @@ +namespace Group17profile.Settings; + +public class ConnectionStrings +{ + public string? DbConnectionString { get; set; } + public string? AzureBlobStorage { get; set; } +} \ No newline at end of file diff --git a/appsettings.json b/appsettings.json index 7cae4b15b859c2896e72c106cbd4b9b09d96fcce..a837c297cea5affe97b646d71b20c1c184450dbb 100644 --- a/appsettings.json +++ b/appsettings.json @@ -1,14 +1,12 @@ { - "ProfileDatabase": { - "ConnectionString": "", - "DatabaseName": "Profile-db" - }, "Logging": { "LogLevel": { "Default": "Information", "Microsoft.AspNetCore": "Warning" } }, - "AllowedHosts": "*" -} - + "ConnectionStrings": { + "DbConnectionString": "", + "AzureBlobStorage": "" + } +} \ No newline at end of file