From a82878656fa0bd91c7c39f52bac73166d68880dd Mon Sep 17 00:00:00 2001 From: Adiv <asifadiv@gmail.com> Date: Tue, 11 Apr 2023 14:03:16 +0100 Subject: [PATCH] added banner stuff --- AutoMapperProfile.cs | 14 ++--- Constants.cs | 5 ++ Controllers/DefaultController.cs | 8 +-- Controllers/DefaultProfileController.cs | 5 +- Controllers/ProfileController.cs | 39 ++++++++---- Group17profile.csproj | 55 +++++++++-------- Helpers/ResponseOperationFilter.cs | 8 +-- Middleware/ExceptionMiddleware.cs | 18 +++--- Middleware/SwaggerProfileMiddleware.cs | 8 +-- Models/DTOs/ProfileDTO.cs | 22 +++---- Models/DTOs/UserDTO.cs | 8 +-- Models/DefaultObjects/DefaultDTO.cs | 2 +- Models/DefaultObjects/DefaultEntity.cs | 18 +++--- Models/Entities/Profile.cs | 15 +++-- Models/Entities/User.cs | 2 +- Models/ProfileDbContext.cs | 20 ++++--- Program.cs | 15 ++++- Services/ProfileService.cs | 79 +++++++++++++------------ Services/StorageService.cs | 69 +++++++++++++++++++++ Settings/ConnectionStrings.cs | 7 +++ appsettings.json | 12 ++-- 21 files changed, 265 insertions(+), 164 deletions(-) create mode 100644 Services/StorageService.cs create mode 100644 Settings/ConnectionStrings.cs diff --git a/AutoMapperProfile.cs b/AutoMapperProfile.cs index b875bfc..dfec55e 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 5f08ef3..76bfeff 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 e19bfea..2dce994 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 d681747..ec3012e 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 56bb2e2..b4c8898 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 dfa4874..f2bec7e 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 40ee5d6..5ef82ce 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 e13ec9d..76f8fe5 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 79ed1a4..ed687a5 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 a6727f0..ff8dfec 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 bca9073..d8e2941 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 119ed79..e00c64b 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 1841717..eed48b4 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 38232d1..7dfbad3 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 e7fdb17..586be24 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 bb87b98..2af10fc 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 078397e..3e1a4af 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 c60f5f9..04b6766 100644 --- a/Services/ProfileService.cs +++ b/Services/ProfileService.cs @@ -1,34 +1,35 @@ -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(ProfileDTO profile, int userId); - Task<string> UploadBannerPicture(ProfileDTO profile, int userId, IFormFile bannerPicture); - Task<string> EditBannerPicture(ProfileDTO profile, int userId, IFormFile? bannerPicture = null); + Task<UserProfileDTO> GetProfileForUser(int userId); + Task<string> UploadBannerPicture(int userId, IFormFile bannerPicture); } 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) @@ -37,14 +38,12 @@ public class ProfileService : IProfileService 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) @@ -67,35 +66,41 @@ public class ProfileService : IProfileService { var user = _userRepository.GetByIdThrowIfNull(userId); if (user.ProfileId == null) - throw new ProfileException("Failed to retrieve profile for user"); + throw new ProfileException("Failed to retrieve profile for user."); - var record = _mapper.Map<Profile>(profile); - if (!string.IsNullOrWhiteSpace(record.FavouriteShows)) - { - var s = record.FavouriteShows.Split(','); - var list = s.ToList(); - profile.FavouriteShows = list; - } + 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(); - var profile = _mapper.Map<ProfileDTO>(_profileRepository.GetAll().AsNoTracking() - .FirstOrDefault(p => p.Id == user.ProfileId)); - return new UserProfileDTO() {User = _mapper.Map<UserDTO>(user), Profile = profile}; + return new UserProfileDTO {User = _mapper.Map<UserDTO>(user), Profile = record}; } - public Task<string> UploadBannerPicture(ProfileDTO profile, int userId, IFormFile bannerPicture) + public async Task<string> UploadBannerPicture(int userId, IFormFile bannerPicture) { var user = _userRepository.GetByIdThrowIfNull(userId); - if (profile.BannerUrl == null) - { - } - throw new NotImplementedException(); - } + 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); - public Task<string> EditBannerPicture(ProfileDTO profile, int userId, IFormFile? bannerPicture = null) - { - var user = _userRepository.GetByIdThrowIfNull(userId); + 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()); + } - throw new NotImplementedException(); + 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 0000000..3386e33 --- /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 0000000..0a688ac --- /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 7cae4b1..a837c29 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 -- GitLab