diff --git a/Controllers/AuthenticationController.cs b/Controllers/AuthenticationController.cs index 55a5fd72b517876ca000631ea3f6ad6e7dd26220..d16d97e5a717d9f074c64c7bed34f87ed9001d44 100644 --- a/Controllers/AuthenticationController.cs +++ b/Controllers/AuthenticationController.cs @@ -258,15 +258,6 @@ public class AuthController : BaseAuthController return Ok(null); } - [HttpPost("UploadProfilePicture")] - [Consumes("multipart/form-data")] - public async Task<ActionResult<ResponseEnvelope<string>>> UploadProfilePicture(IFormFile? profilePicture) - { - if (profilePicture == null) - return BadRequest("Please upload a proper image."); - return Ok(await _userService.UploadProfilePicture(UserId, profilePicture)); - } - [AllowAnonymous] [HttpPost("SendEmail")] [SwaggerResponse(204)] @@ -284,4 +275,34 @@ public class AuthController : BaseAuthController return Ok(null); } + + [AllowAnonymous] + [HttpGet("GetUserById")] + [SwaggerResponse(200, Type = typeof(ResponseEnvelope<UserDTO>))] + public async Task<ActionResult<ResponseEnvelope<UserDTO>>> GetUserById(int userId) + { + try + { + return Ok(await _userService.GetUserById(userId)); + } + catch (Exception ex) + { + return BadRequest(ex.Message); + } + } + + [AllowAnonymous] + [HttpPost("GetUsersById")] + [SwaggerResponse(200, Type = typeof(ResponseEnvelope<List<UserDTO>>))] + public async Task<ActionResult<ResponseEnvelope<List<UserDTO>>>> GetUsersById(List<int> userIds) + { + try + { + return Ok(await _userService.GetUsersById(userIds)); + } + catch (Exception ex) + { + return BadRequest(ex.Message); + } + } } \ No newline at end of file diff --git a/Migrations/20230505160442_RemovedProfilePictures.Designer.cs b/Migrations/20230505160442_RemovedProfilePictures.Designer.cs new file mode 100644 index 0000000000000000000000000000000000000000..690d66f13554086c288bf9c250a721119fe0485b --- /dev/null +++ b/Migrations/20230505160442_RemovedProfilePictures.Designer.cs @@ -0,0 +1,143 @@ +// <auto-generated /> +using System; +using AuthenticationMicroservice.Models; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace AuthenticationMicroservice.Migrations +{ + [DbContext(typeof(AuthenticationDbContext))] + [Migration("20230505160442_RemovedProfilePictures")] + partial class RemovedProfilePictures + { + /// <inheritdoc /> + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "7.0.5") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("AuthenticationMicroservice.Models.Entities.RefreshToken", b => + { + b.Property<Guid>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier") + .HasDefaultValueSql("newid()"); + + b.Property<DateTimeOffset>("CreatedAt") + .HasColumnType("datetimeoffset"); + + b.Property<bool>("Deleted") + .HasColumnType("bit"); + + b.Property<DateTimeOffset?>("DeletedAt") + .HasColumnType("datetimeoffset"); + + b.Property<long>("Expires") + .HasColumnType("bigint"); + + b.Property<string>("IpAddress") + .HasColumnType("nvarchar(max)"); + + b.Property<bool>("IsValid") + .HasColumnType("bit"); + + b.Property<string>("Reason") + .HasColumnType("nvarchar(max)"); + + b.Property<string>("Token") + .HasColumnType("nvarchar(max)"); + + b.Property<DateTimeOffset?>("UpdatedAt") + .HasColumnType("datetimeoffset"); + + b.Property<int>("UserId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.HasIndex("CreatedAt", "Deleted"); + + b.ToTable("RefreshToken"); + }); + + modelBuilder.Entity("AuthenticationMicroservice.Models.Entities.User", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id")); + + b.Property<DateTimeOffset>("CreatedAt") + .HasColumnType("datetimeoffset"); + + b.Property<DateTimeOffset?>("DeletedAt") + .HasColumnType("datetimeoffset"); + + b.Property<string>("EmailAddress") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.Property<Guid?>("EmailConfirmationToken") + .HasColumnType("uniqueidentifier"); + + b.Property<string>("FirstName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property<DateTimeOffset?>("LastLogin") + .HasColumnType("datetimeoffset"); + + b.Property<DateTimeOffset?>("LastRefreshTokenIssued") + .HasColumnType("datetimeoffset"); + + b.Property<string>("Password") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property<Guid?>("PasswordResetToken") + .HasColumnType("uniqueidentifier"); + + b.Property<string>("Surname") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property<string>("UnconfirmedEmail") + .HasColumnType("nvarchar(max)"); + + b.Property<DateTimeOffset?>("UpdatedAt") + .HasColumnType("datetimeoffset"); + + b.HasKey("Id"); + + b.HasIndex("EmailAddress") + .IsUnique(); + + b.ToTable("User"); + }); + + modelBuilder.Entity("AuthenticationMicroservice.Models.Entities.RefreshToken", b => + { + b.HasOne("AuthenticationMicroservice.Models.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("User"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Migrations/20230505160442_RemovedProfilePictures.cs b/Migrations/20230505160442_RemovedProfilePictures.cs new file mode 100644 index 0000000000000000000000000000000000000000..ea5ea159ef407a3e9cff8d07a0c147709438ecb5 --- /dev/null +++ b/Migrations/20230505160442_RemovedProfilePictures.cs @@ -0,0 +1,28 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace AuthenticationMicroservice.Migrations +{ + /// <inheritdoc /> + public partial class RemovedProfilePictures : Migration + { + /// <inheritdoc /> + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "ProfilePictureUrl", + table: "User"); + } + + /// <inheritdoc /> + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn<string>( + name: "ProfilePictureUrl", + table: "User", + type: "nvarchar(max)", + nullable: true); + } + } +} diff --git a/Migrations/AuthenticationDbContextModelSnapshot.cs b/Migrations/AuthenticationDbContextModelSnapshot.cs index f7924edf0003e8bf1b64c83c223acedd3ad71ae7..eeaf1e22f218af9cb685bbfc7b348403b7d583a5 100644 --- a/Migrations/AuthenticationDbContextModelSnapshot.cs +++ b/Migrations/AuthenticationDbContextModelSnapshot.cs @@ -17,7 +17,7 @@ namespace AuthenticationMicroservice.Migrations { #pragma warning disable 612, 618 modelBuilder - .HasAnnotation("ProductVersion", "8.0.0-preview.2.23128.3") + .HasAnnotation("ProductVersion", "7.0.5") .HasAnnotation("Relational:MaxIdentifierLength", 128); SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); @@ -106,9 +106,6 @@ namespace AuthenticationMicroservice.Migrations b.Property<Guid?>("PasswordResetToken") .HasColumnType("uniqueidentifier"); - b.Property<string>("ProfilePictureUrl") - .HasColumnType("nvarchar(max)"); - b.Property<string>("Surname") .IsRequired() .HasColumnType("nvarchar(max)"); diff --git a/Models/DTOs/UserDTO.cs b/Models/DTOs/UserDTO.cs index dd138dc1e34d541da72e82df28b2b417fc40b79a..a119119429c670378f6eabb8540c342938136a29 100644 --- a/Models/DTOs/UserDTO.cs +++ b/Models/DTOs/UserDTO.cs @@ -16,8 +16,6 @@ public class UserDTO : BaseIntDto public DateTimeOffset? LastLogin { get; set; } public DateTimeOffset? LastRefreshTokenIssued { get; set; } - public string? ProfilePictureUrl { get; set; } - public string? UnconfirmedEmail { get; set; } } diff --git a/Models/Entities/User.cs b/Models/Entities/User.cs index b73b59935783d822c6b8cd02f509416f22c2d723..544edc9d9a73719c870c037259db1f53f9f98c76 100644 --- a/Models/Entities/User.cs +++ b/Models/Entities/User.cs @@ -17,8 +17,6 @@ public class User : BaseIntEntity public DateTimeOffset? LastLogin { get; set; } public DateTimeOffset? LastRefreshTokenIssued { get; set; } - public string? ProfilePictureUrl { get; set; } - public string? UnconfirmedEmail { get; set; } public Guid? PasswordResetToken { get; set; } diff --git a/Program.cs b/Program.cs index 01deefb6321e5e72d06b15419b3f67d7ada60374..c6ad4e2755c4e3ec21ca05dd18fae43b2948731f 100644 --- a/Program.cs +++ b/Program.cs @@ -114,7 +114,6 @@ builder.Services.AddSingleton(mapper); builder.Services.AddTransient(typeof(IBaseRepository<>), typeof(BaseRepository<>)); // Add services from /Services -builder.Services.AddTransient<IStorageService, StorageService>(); builder.Services.AddTransient<IUserService, UserService>(); builder.Services.AddTransient<IRefreshTokenService, RefreshTokenService>(); builder.Services.AddTransient<IEmailService, EmailService>(); diff --git a/Services/AuthService.cs b/Services/AuthService.cs index f1a4ce99adefa42d26913278dfe4a6694d7b8f19..3826dd8c3b6326a3e44f91717edb35ba1c938488 100644 --- a/Services/AuthService.cs +++ b/Services/AuthService.cs @@ -29,14 +29,12 @@ public class AuthService : IAuthService private readonly AuthenticationDbContext _context; private readonly IEmailService _emailService; private readonly IRefreshTokenService _refreshTokenService; - private readonly IStorageService _storageService; public AuthService(AuthenticationDbContext context, IEmailService emailService, - IRefreshTokenService refreshTokenService, IStorageService storageService) + IRefreshTokenService refreshTokenService) { _context = context; _emailService = emailService; - _storageService = storageService; _refreshTokenService = refreshTokenService; } @@ -79,9 +77,6 @@ public class AuthService : IAuthService AccessTokenExpiresIn = newAccess.ExpiresIn, RefreshToken = newRefresh!.Token, RefreshTokenExpires = newRefresh.Expires, - ProfilePictureUrl = user.ProfilePictureUrl, - ProfilePictureSas = _storageService.GetSasForFile(Constants.AzureBlobContainers.ProfilePictures, - user.ProfilePictureUrl ?? string.Empty), UserId = user.Id }; diff --git a/Services/StorageService.cs b/Services/StorageService.cs deleted file mode 100644 index 9e4bbf626e2dcdfdd04415f9f59e8a57cbfeee0e..0000000000000000000000000000000000000000 --- a/Services/StorageService.cs +++ /dev/null @@ -1,70 +0,0 @@ -namespace AuthenticationMicroservice.Services; - -using System.Web; -using Azure.Storage.Blobs; -using Azure.Storage.Blobs.Models; -using Azure.Storage.Sas; -using Microsoft.Extensions.Options; -using Settings; - -public interface IStorageService -{ - Task<Uri> SaveImageAsJpgBlob(string containerName, string fileName, Stream input, - Dictionary<string, string>? metadata = null); - - string? GetSasForFile(string containerName, string url, DateTimeOffset? expires = 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 null; - var container = new BlobContainerClient(_connectionStrings.AzureBlobStorage, containerName); - var blob = container.GetBlobClient(url); - var urlWithSas = blob.GenerateSasUri(BlobSasPermissions.Read, DateTimeOffset.Now.AddHours(3)); - return urlWithSas.Query; - } - - 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(1366, 768)})); - await clone.SaveAsJpegAsync(croppedImage); - croppedImage.Position = 0; - var blob = await GetBlobReference(containerName, fileName); - await blob.UploadAsync(croppedImage, new BlobHttpHeaders {ContentType = "image/jpeg"}); - 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.IsNullOrEmpty(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/Services/UserService.cs b/Services/UserService.cs index 626784bc068e510e354a4b624f8371fc563b43e0..a24e43f71f5cb7030bd711d03aa1b7bc570b0eed 100644 --- a/Services/UserService.cs +++ b/Services/UserService.cs @@ -13,21 +13,20 @@ using Repositories; public interface IUserService { Task<User> CreateUser(UserRegisterRequestDTO request); - Task<string?> UploadProfilePicture(int userId, IFormFile profilePicture); + Task<UserDTO> GetUserById(int userId); + Task<List<UserDTO>> GetUsersById(List<int> userIds); List<UserDTO> GetListOfAllUsers(); } public class UserService : IUserService { private readonly IMapper _mapper; - private readonly IStorageService _storageService; private readonly IBaseRepository<User> _userRepo; - public UserService(IMapper mapper, IBaseRepository<User> userRepo, IStorageService storageService) + public UserService(IMapper mapper, IBaseRepository<User> userRepo) { _mapper = mapper; _userRepo = userRepo; - _storageService = storageService; } public async Task<User> CreateUser(UserRegisterRequestDTO request) @@ -52,18 +51,15 @@ public class UserService : IUserService return await _userRepo.CreateAndSaveAsync(newUser); } - public async Task<string?> UploadProfilePicture(int userId, IFormFile profilePicture) + public async Task<UserDTO> GetUserById(int userId) { - var user = _userRepo.GetByIdThrowIfNull(userId); - using var ms = new MemoryStream(); - await profilePicture.CopyToAsync(ms); - ms.Position = 0; - var fileName = $"{user.Id}/{user.FirstName}"; - var image = await _storageService.SaveImageAsJpgBlob(Constants.AzureBlobContainers.ProfilePictures, fileName, - ms); - user.ProfilePictureUrl = image.ToString(); - await _userRepo.UpdateAndSaveAsync(user); - return _storageService.GetSasForFile(Constants.AzureBlobContainers.ProfilePictures, image.ToString()); + return _mapper.Map<UserDTO>(_userRepo.GetByIdThrowIfNull(userId)); + } + + public async Task<List<UserDTO>> GetUsersById(List<int> userIds) + { + var users = _userRepo.GetAll().AsNoTracking().Where(u => userIds.Contains(u.Id)); + return await _mapper.ProjectTo<UserDTO>(users).ToListAsync(); } public List<UserDTO> GetListOfAllUsers() diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 58440179421390802c6b6fe42de94ea723015c90..e171fe33ac1f284e9d3ca3e37951460d56875418 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -3,10 +3,10 @@ # https://docs.microsoft.com/azure/devops/pipelines/languages/docker trigger: -- main + - main resources: -- repo: self + - repo: self variables: # Container registry service connection established during pipeline creation @@ -20,20 +20,20 @@ variables: vmImageName: 'ubuntu-latest' stages: -- stage: Build - displayName: Build and push stage - jobs: - - job: Build - displayName: Build - pool: - vmImage: $(vmImageName) - steps: - - task: Docker@2 - displayName: Build and push an image to container registry - inputs: - command: buildAndPush - repository: $(imageRepository) - dockerfile: $(dockerfilePath) - containerRegistry: $(dockerRegistryServiceConnection) - tags: | - $(tag) + - stage: Build + displayName: Build and push stage + jobs: + - job: Build + displayName: Build + pool: + vmImage: $(vmImageName) + steps: + - task: Docker@2 + displayName: Build and push an image to container registry + inputs: + command: buildAndPush + repository: $(imageRepository) + dockerfile: $(dockerfilePath) + containerRegistry: $(dockerRegistryServiceConnection) + tags: | + $(tag)