diff --git a/API/API.csproj b/API/API.csproj index f8ed32e..99e95d1 100644 --- a/API/API.csproj +++ b/API/API.csproj @@ -11,6 +11,7 @@ + diff --git a/API/Application/Users/Commands/CreateUser.cs b/API/Application/Users/Commands/CreateUser.cs index e271b8d..7e01ef6 100644 --- a/API/Application/Users/Commands/CreateUser.cs +++ b/API/Application/Users/Commands/CreateUser.cs @@ -82,9 +82,10 @@ namespace API.Application.Users.Commands CreatedAt = DateTime.UtcNow.AddHours(2), UpdatedAt = DateTime.UtcNow.AddHours(2), HashedPassword = hashedPassword, + ProfilePicture = "", RefreshToken = System.Guid.NewGuid().ToString(), RefreshTokenExpiresAt = DateTime.UtcNow.AddDays(7), - }; + }; } } } diff --git a/API/Application/Users/Commands/UpdateUser.cs b/API/Application/Users/Commands/UpdateUser.cs index 16ec259..0c1871c 100644 --- a/API/Application/Users/Commands/UpdateUser.cs +++ b/API/Application/Users/Commands/UpdateUser.cs @@ -1,5 +1,6 @@ using API.Models; using API.Persistence.Repositories; +using API.Persistence.Services; using Microsoft.AspNetCore.Mvc; using System.Text.RegularExpressions; @@ -8,10 +9,16 @@ namespace API.Application.Users.Commands public class UpdateUser { private readonly IUserRepository _repository; + private readonly R2Service _r2Service; + private readonly string _accessKey; + private readonly string _secretKey; - public UpdateUser(IUserRepository repository) + public UpdateUser(IUserRepository repository, AppConfiguration config) { _repository = repository; + _accessKey = config.AccessKey; + _secretKey = config.SecretKey; + _r2Service = new R2Service(_accessKey, _secretKey); } public async Task Handle(UpdateUserDTO updateUserDTO) @@ -31,22 +38,41 @@ namespace API.Application.Users.Commands return new ConflictObjectResult(new { message = "Email is already in use." }); } } - if (updateUserDTO.Password != "") + if (updateUserDTO.Password != "½") { if (IsPasswordSecure(updateUserDTO.Password)) { string hashedPassword = BCrypt.Net.BCrypt.HashPassword(updateUserDTO.Password); currentUser.HashedPassword = hashedPassword; } + else + { + return new ConflictObjectResult(new { message = "Password is not secure." }); + } } - if (updateUserDTO.Username != "") + if (updateUserDTO.Username != "½") currentUser.Username = updateUserDTO.Username; - if (updateUserDTO.Email != "") + if (updateUserDTO.Email != "½") currentUser.Email = updateUserDTO.Email; + string imageUrl = null; + if (updateUserDTO.ProfilePicture != null && updateUserDTO.ProfilePicture.Length > 0) + { + try + { + using (var fileStream = updateUserDTO.ProfilePicture.OpenReadStream()) + { + imageUrl = await _r2Service.UploadToR2(fileStream, "PP" + updateUserDTO.Id); + currentUser.ProfilePicture = imageUrl; + } + } + catch (Exception ex) + { + return new StatusCodeResult(StatusCodes.Status500InternalServerError); + } - + } bool success = await _repository.UpdateUserAsync(currentUser); if (success) diff --git a/API/Application/Users/Queries/QueryAllUsers.cs b/API/Application/Users/Queries/QueryAllUsers.cs index 4d74f42..0681417 100644 --- a/API/Application/Users/Queries/QueryAllUsers.cs +++ b/API/Application/Users/Queries/QueryAllUsers.cs @@ -25,7 +25,8 @@ namespace API.Application.Users.Queries { Id = user.Id, Email = user.Email, - Username = user.Username + Username = user.Username, + ProfilePictureURL = user.ProfilePicture, }).ToList(); return userDTOs; } diff --git a/API/Application/Users/Queries/QueryUserById.cs b/API/Application/Users/Queries/QueryUserById.cs index d3a6d6b..265973a 100644 --- a/API/Application/Users/Queries/QueryUserById.cs +++ b/API/Application/Users/Queries/QueryUserById.cs @@ -24,7 +24,7 @@ namespace API.Application.Users.Queries return new ConflictObjectResult(new { message = "No user on given Id" }); } - return new OkObjectResult(new { id = user.Id, email = user.Email, username = user.Username, createdAt = user.CreatedAt }); + return new OkObjectResult(new { id = user.Id, email = user.Email, username = user.Username, profilePictureURL = user.ProfilePicture, createdAt = user.CreatedAt }); } diff --git a/API/Controllers/UsersController.cs b/API/Controllers/UsersController.cs index 8c83fa0..ac0c6d4 100644 --- a/API/Controllers/UsersController.cs +++ b/API/Controllers/UsersController.cs @@ -12,6 +12,7 @@ using System.Text.RegularExpressions; using Helpers; using Microsoft.AspNetCore.Identity; using API.Persistence.Repositories; +using API.Persistence.Services; namespace API.Controllers { @@ -26,6 +27,8 @@ namespace API.Controllers private readonly DeleteUser _deleteUser; private readonly LoginUser _loginUser; private readonly TokenHelper _tokenHelper; + + private readonly IUserRepository _repository; public UsersController( @@ -36,7 +39,8 @@ namespace API.Controllers DeleteUser deleteUser, LoginUser loginUser, TokenHelper tokenHelper, - IUserRepository repository) + IUserRepository repository + ) { _queryAllUsers = queryAllUsers; _queryUserById = queryUserById; @@ -46,6 +50,8 @@ namespace API.Controllers _loginUser = loginUser; _tokenHelper = tokenHelper; _repository = repository; + + } [HttpPost("login")] @@ -69,7 +75,7 @@ namespace API.Controllers [Authorize] [HttpPut] - public async Task PutUser(UpdateUserDTO UpdateUserDTO) + public async Task PutUser([FromForm] UpdateUserDTO UpdateUserDTO) { return await _updateUser.Handle(UpdateUserDTO); } diff --git a/API/Migrations/20240904110821_addedprofilepicture.Designer.cs b/API/Migrations/20240904110821_addedprofilepicture.Designer.cs new file mode 100644 index 0000000..89e6f11 --- /dev/null +++ b/API/Migrations/20240904110821_addedprofilepicture.Designer.cs @@ -0,0 +1,62 @@ +// +using System; +using API; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace API.Migrations +{ + [DbContext(typeof(AppDBContext))] + [Migration("20240904110821_addedprofilepicture")] + partial class addedprofilepicture + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "8.0.7"); + + modelBuilder.Entity("API.Models.User", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("Email") + .HasColumnType("TEXT"); + + b.Property("HashedPassword") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("ProfilePicture") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("RefreshToken") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("RefreshTokenExpiresAt") + .HasColumnType("TEXT"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.Property("Username") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("Users"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/API/Migrations/20240904110821_addedprofilepicture.cs b/API/Migrations/20240904110821_addedprofilepicture.cs new file mode 100644 index 0000000..325e323 --- /dev/null +++ b/API/Migrations/20240904110821_addedprofilepicture.cs @@ -0,0 +1,29 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace API.Migrations +{ + /// + public partial class addedprofilepicture : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "ProfilePicture", + table: "Users", + type: "TEXT", + nullable: false, + defaultValue: ""); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "ProfilePicture", + table: "Users"); + } + } +} diff --git a/API/Migrations/AppDBContextModelSnapshot.cs b/API/Migrations/AppDBContextModelSnapshot.cs index 9bb3e04..ac297d8 100644 --- a/API/Migrations/AppDBContextModelSnapshot.cs +++ b/API/Migrations/AppDBContextModelSnapshot.cs @@ -32,6 +32,10 @@ namespace API.Migrations .IsRequired() .HasColumnType("TEXT"); + b.Property("ProfilePicture") + .IsRequired() + .HasColumnType("TEXT"); + b.Property("RefreshToken") .IsRequired() .HasColumnType("TEXT"); diff --git a/API/Models/User.cs b/API/Models/User.cs index bfb9b09..5eb7937 100644 --- a/API/Models/User.cs +++ b/API/Models/User.cs @@ -8,6 +8,7 @@ public class User : BaseModel public string? Username { get; set; } public string HashedPassword { get; set; } public string RefreshToken { get; set; } + public string ProfilePicture { get; set; } public DateTime RefreshTokenExpiresAt { get; set; } } @@ -16,6 +17,8 @@ public class UserDTO public string Id { get; set; } public string Email { get; set; } public string Username { get; set; } + public string ProfilePictureURL { get; set; } + } public class LoginDTO @@ -37,6 +40,8 @@ public class UpdateUserDTO public string Email { get; set; } public string Username { get; set; } public string Password { get; set; } + public IFormFile ProfilePicture { get; set; } + } public class RefreshTokenDTO diff --git a/API/Persistence/Services/AppConfiguration.cs b/API/Persistence/Services/AppConfiguration.cs new file mode 100644 index 0000000..97265f1 --- /dev/null +++ b/API/Persistence/Services/AppConfiguration.cs @@ -0,0 +1,8 @@ +namespace API.Persistence.Services +{ + public class AppConfiguration + { + public string AccessKey { get; set; } + public string SecretKey { get; set; } + } +} diff --git a/API/Persistence/Services/R2Service.cs b/API/Persistence/Services/R2Service.cs new file mode 100644 index 0000000..c99f844 --- /dev/null +++ b/API/Persistence/Services/R2Service.cs @@ -0,0 +1,48 @@ +using Amazon.Runtime; +using Amazon.S3.Model; +using Amazon.S3; + +namespace API.Persistence.Services +{ + public class R2Service + { + private readonly IAmazonS3 _s3Client; + public string AccessKey { get; } + public string SecretKey { get; } + + public R2Service(string accessKey, string secretKey) + { + AccessKey = accessKey; + SecretKey = secretKey; + + var credentials = new BasicAWSCredentials(accessKey, secretKey); + var config = new AmazonS3Config + { + ServiceURL = "https://a6051dbbe0af70488aff47b9f4d9fc1c.r2.cloudflarestorage.com", + ForcePathStyle = true + }; + _s3Client = new AmazonS3Client(credentials, config); + } + + public async Task UploadToR2(Stream fileStream, string fileName) + { + var request = new PutObjectRequest + { + InputStream = fileStream, + BucketName = "h4fil", + Key = fileName, + DisablePayloadSigning = true + }; + + var response = await _s3Client.PutObjectAsync(request); + + if (response.HttpStatusCode != System.Net.HttpStatusCode.OK) + { + throw new AmazonS3Exception($"Error uploading file to S3. HTTP Status Code: {response.HttpStatusCode}"); + } + + var imageUrl = $"https://h4file.magsapi.com/{fileName}"; + return imageUrl; + } + } +} diff --git a/API/Program.cs b/API/Program.cs index 7393198..7566934 100644 --- a/API/Program.cs +++ b/API/Program.cs @@ -7,6 +7,7 @@ using Microsoft.IdentityModel.Tokens; using Microsoft.OpenApi.Models; using System.Text; using Helpers; +using API.Persistence.Services; namespace API { @@ -69,6 +70,8 @@ namespace API }; }); + + // Swagger does not by default allow to use Bearer tokens // The method AddSwaggerGen with the following options grants access to address a Bearer token - // Simply by clicking the Lock icon and pasting the Bearer Token @@ -105,6 +108,15 @@ namespace API Console.WriteLine("Connecting to database with connection string: " + connectionString); + var accessKey = Configuration["AccessKey"] ?? Environment.GetEnvironmentVariable("ACCESS_KEY"); + var secretKey = Configuration["SecretKey"] ?? Environment.GetEnvironmentVariable("SECRET_KEY"); + + builder.Services.AddSingleton(new AppConfiguration + { + AccessKey = accessKey, + SecretKey = secretKey + }); + var app = builder.Build(); // Configure the HTTP request pipeline.