diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000000000000000000000000000000000000..2078352f0df0921a45f81e039046beccf6cd6ba7 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,26 @@ +**/.dockerignore +**/.env +**/.git +**/.gitignore +**/.project +**/.settings +**/.toolstarget +**/.vs +**/.vscode +**/.idea +**/*.*proj.user +**/*.dbmdl +**/*.jfm +**/azds.yaml +**/bin +**/charts +**/docker-compose* +**/Dockerfile* +**/node_modules +**/npm-debug.log +**/obj +**/secrets.dev.yaml +**/values.dev.yaml +appsettings.Development.json +LICENSE +README.md \ No newline at end of file diff --git a/AutoMapperProfile.cs b/AutoMapperProfile.cs index dfec55e8dccd2f861d6b63cf9530399b8c7c0aea..1cff8a6bf39c0c521bb53a16dfe4a2331f4eac88 100644 --- a/AutoMapperProfile.cs +++ b/AutoMapperProfile.cs @@ -1,15 +1,12 @@ namespace Group17profile; +using AutoMapper; using Models.DTOs; -using Models.Entities; -using Profile = AutoMapper.Profile; public class AutoMapperProfile : Profile { public AutoMapperProfile() { - CreateMap<User, UserDTO>(); - CreateMap<UserDTO, User>(); CreateMap<Models.Entities.Profile, ProfileDTO>().ForMember(opt => opt.FavouriteShows, src => src.Ignore()); CreateMap<ProfileDTO, Models.Entities.Profile>().ForMember(opt => opt.FavouriteShows, src => src.Ignore()); } diff --git a/Controllers/AuthorizeRolesAttribute.cs b/Controllers/AuthorizeRolesAttribute.cs new file mode 100644 index 0000000000000000000000000000000000000000..d70c4e78d96a9bfb489db409d69ec692f6175dc6 --- /dev/null +++ b/Controllers/AuthorizeRolesAttribute.cs @@ -0,0 +1,32 @@ +namespace Group17profile.Controllers; + +using System.Net; +using System.Security.Claims; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Filters; + +[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true)] +public class AuthorizeRoles : AuthorizeAttribute, IAuthorizationFilter +{ + private readonly Constants.Roles[] _roles; + + public AuthorizeRoles(params Constants.Roles[] roles) + { + _roles = roles; + } + + public void OnAuthorization(AuthorizationFilterContext context) + { + var user = context.HttpContext.User; + + if (user.Identity is {IsAuthenticated: false}) + return; + + + if (_roles.Any(role => user.Claims.Any(c => c.Type == ClaimTypes.Role && c.Value == role.ToString()))) + return; + + context.Result = new StatusCodeResult((int) HttpStatusCode.Forbidden); + } +} \ No newline at end of file diff --git a/Controllers/DefaultProfileController.cs b/Controllers/DefaultAuthenticationController.cs similarity index 69% rename from Controllers/DefaultProfileController.cs rename to Controllers/DefaultAuthenticationController.cs index ec3012e789294ebf9a3079122fe296cf58a477c9..b05fc8c1baa076da7f18e39c0e65fa89d2599d81 100644 --- a/Controllers/DefaultProfileController.cs +++ b/Controllers/DefaultAuthenticationController.cs @@ -1,8 +1,10 @@ namespace Group17profile.Controllers; using System.Security.Claims; +using Microsoft.AspNetCore.Authorization; -public class DefaultProfileController : DefaultController +[Authorize] +public class DefaultAuthenticationController : DefaultController { protected int UserId { @@ -28,19 +30,4 @@ public class DefaultProfileController : DefaultController private bool IsServer => User.HasClaim(c => c.Type == ClaimTypes.Role && c.Value == Constants.Roles.Server.ToString()); - - protected string? GetAccessToken - { - get { return User.Claims.FirstOrDefault(c => c.Type == Constants.Tokens.AccessToken)?.Value; } - } - - protected string? GetRefreshToken - { - get { return User.Claims.FirstOrDefault(c => c.Type == Constants.Tokens.RefreshToken)?.Value; } - } - - protected string? GetProfileSasToken - { - get { return User.Claims.FirstOrDefault(c => c.Type == Constants.Tokens.ProfileSasToken)?.Value; } - } } \ No newline at end of file diff --git a/Controllers/DefaultController.cs b/Controllers/DefaultController.cs index 2dce994645a389916717549523f6f23d2b60005d..632c716957c467c2aa3363cc47bee47733732900 100644 --- a/Controllers/DefaultController.cs +++ b/Controllers/DefaultController.cs @@ -1,9 +1,12 @@ namespace Group17profile.Controllers; using System.Reflection; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Middleware; +[ApiController] +[Authorize] public class DefaultController : Controller { public OkObjectResult Ok<T>(T value) diff --git a/Controllers/ProfileController.cs b/Controllers/ProfileController.cs index b4c8898c11f0cdae47ab3644041ec03b11e084c5..7247d8fbffaf4a4b8b83e871086c077960d13dd2 100644 --- a/Controllers/ProfileController.cs +++ b/Controllers/ProfileController.cs @@ -8,7 +8,7 @@ using Swashbuckle.AspNetCore.Annotations; [Route("api/[controller]")] [ApiController] -public class ProfileController : DefaultProfileController +public class ProfileController : DefaultAuthenticationController { private readonly IProfileService _profileService; @@ -18,9 +18,9 @@ public class ProfileController : DefaultProfileController } [HttpGet("GetProfileForUser")] - [SwaggerResponse(200, Type = typeof(ResponseEnvelope<UserProfileDTO>))] + [SwaggerResponse(200, Type = typeof(ResponseEnvelope<ProfileDTO>))] [SwaggerResponse(400, Type = typeof(ResponseEnvelope<BadRequestObjectResult>))] - public async Task<ActionResult<ResponseEnvelope<UserProfileDTO>>> GetProfileForUser() + public async Task<ActionResult<ResponseEnvelope<ProfileDTO>>> GetProfileForUser() { try { @@ -39,7 +39,7 @@ public class ProfileController : DefaultProfileController { try { - var newProfile = await _profileService.CreateOrUpdateProfile(profile, 1); + var newProfile = await _profileService.CreateOrUpdateProfile(profile, UserId); return Ok(newProfile); } catch (Exception ex) @@ -50,10 +50,10 @@ public class ProfileController : DefaultProfileController [HttpPost("UploadBannerPicture")] [Consumes("multipart/form-data")] - public async Task<ActionResult<ResponseEnvelope<string>>> UploadBannerPicture(IFormFile? profilePicture) + public async Task<ActionResult<ResponseEnvelope<ProfileDTO>>> UploadBannerPicture(IFormFile? profilePicture) { if (profilePicture == null) return BadRequest("Please upload a file."); - return Ok(await _profileService.UploadBannerPicture(1, profilePicture)); + return Ok(await _profileService.UploadBannerPicture(UserId, profilePicture)); } } \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..1f212afd256cf3fd7d6c114c18e83622aae894a2 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,22 @@ +FROM mcr.microsoft.com/dotnet/sdk:8.0.100-preview.3 AS base +WORKDIR /app +EXPOSE 80 +EXPOSE 8080 +EXPOSE 5003 +EXPOSE 443 + +FROM mcr.microsoft.com/dotnet/sdk:8.0.100-preview.3 AS build +WORKDIR /src +COPY ["Group17profile.csproj", "./"] +RUN dotnet restore "Group17profile.csproj" +COPY . . +WORKDIR "/src/" +RUN dotnet build "Group17profile.csproj" -c Release -o /app/build + +FROM build AS publish +RUN dotnet publish "Group17profile.csproj" -c Release -o /app/publish /p:UseAppHost=false + +FROM base AS final +WORKDIR /app +COPY --from=publish /app/publish . +ENTRYPOINT ["dotnet", "Group17profile.dll"] diff --git a/Group17profile.csproj b/Group17profile.csproj index 1c2942e4b35183308fbbc688d166e5f39616205e..06635375d280a664b48edb38263b402ea7f53ba4 100644 --- a/Group17profile.csproj +++ b/Group17profile.csproj @@ -4,30 +4,30 @@ <TargetFramework>net8.0</TargetFramework> <Nullable>enable</Nullable> <ImplicitUsings>enable</ImplicitUsings> + <DockerDefaultTargetOS>Linux</DockerDefaultTargetOS> </PropertyGroup> <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="7.0.5" /> - <PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="7.0.4" /> - <PackageReference Include="Microsoft.Azure.Cosmos" Version="3.32.3" /> - <PackageReference Include="Microsoft.EntityFrameworkCore" Version="7.0.5" /> - <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="7.0.5"> - <PrivateAssets>all</PrivateAssets> - <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> - </PackageReference> - <PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="7.0.5" /> - <PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="7.0.5"> - <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" /> + <PackageReference Include="AutoMapper" Version="12.0.1"/> + <PackageReference Include="AutoMapper.Extensions.Microsoft.DependencyInjection" Version="12.0.0"/> + <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.Authentication.JwtBearer" Version="7.0.3"/> + <PackageReference Include="Microsoft.AspNetCore.Authorization" Version="7.0.5"/> + <PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="7.0.4"/> + <PackageReference Include="Microsoft.Azure.Cosmos" Version="3.32.3"/> + <PackageReference Include="Newtonsoft.Json" Version="13.0.3"/> + <PackageReference Include="Microsoft.Extensions.Azure" Version="1.6.3"/> + <PackageReference Include="Microsoft.Extensions.Logging" Version="8.0.0-preview.3.23174.8"/> + <PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="8.0.0-preview.3.23174.8"/> + <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"/> + <PackageReference Include="Swashbuckle.AspNetCore.Newtonsoft" Version="6.5.0"/> + </ItemGroup> + + <ItemGroup> + <Folder Include="Migrations\"/> </ItemGroup> </Project> diff --git a/Helpers/AuthResponseOperationFilter.cs b/Helpers/AuthResponseOperationFilter.cs new file mode 100644 index 0000000000000000000000000000000000000000..9c0b1a5723454b68facf0e6a878c3c808b6d09db --- /dev/null +++ b/Helpers/AuthResponseOperationFilter.cs @@ -0,0 +1,81 @@ +namespace Group17profile.helpers; + +using System.Reflection; +using Controllers; +using Microsoft.AspNetCore.Authorization; +using Microsoft.OpenApi.Models; +using Swashbuckle.AspNetCore.SwaggerGen; + +public class AuthResponsesOperationFilter : IOperationFilter +{ + public void Apply(OpenApiOperation operation, OperationFilterContext context) + { + var authAttributes = context.MethodInfo.DeclaringType?.GetCustomAttributes(true) + .Union(context.MethodInfo.GetCustomAttributes(true)) + .Union(context.MethodInfo.ReflectedType.GetCustomAttributes(true)) + .OfType<AuthorizeAttribute>(); + + var anonAttributes = context.MethodInfo.DeclaringType?.GetCustomAttributes(true) + .Union(context.MethodInfo.GetCustomAttributes(true)) + .Union(context.MethodInfo.ReflectedType.GetCustomAttributes(true)) + .OfType<AllowAnonymousAttribute>(); + + if (authAttributes != null && authAttributes.Any() && !operation.Responses.TryGetValue("401", out _)) + operation.Responses.Add("401", new OpenApiResponse {Description = "Unauthorized"}); + + if (authAttributes != null && authAttributes.Any() && !operation.Responses.TryGetValue("403", out _)) + operation.Responses.Add("403", new OpenApiResponse {Description = "Forbidden"}); + + if (authAttributes != null && authAttributes.Any() && anonAttributes != null && !anonAttributes.Any()) + operation.Security.Add(new OpenApiSecurityRequirement + { + { + new OpenApiSecurityScheme + { + Reference = new OpenApiReference + { + Id = "Bearer", + Type = ReferenceType.SecurityScheme + } + }, + new List<string>() + } + }); + + var apiTokenAttributes = context.MethodInfo.DeclaringType?.GetCustomAttributes(true) + .Union(context.MethodInfo.GetCustomAttributes(true)) + .Union(context.MethodInfo.ReflectedType.GetCustomAttributes(true)) + .OfType<AuthorizeRoles>(); + + if (apiTokenAttributes != null && apiTokenAttributes.Any() && anonAttributes != null && !anonAttributes.Any()) + { + var roleField = apiTokenAttributes.First().GetType() + .GetField("roles", BindingFlags.NonPublic | BindingFlags.Instance); + + if (roleField == null) + return; + +#pragma warning disable CS8600 // Converting null literal or possible null value to non-nullable type. + var roles = (Constants.Roles[]) roleField.GetValue(apiTokenAttributes.First()); +#pragma warning restore CS8600 // Converting null literal or possible null value to non-nullable type. + + if (roles != null && roles.Contains(Constants.Roles.Api)) + operation.Security.Add(new OpenApiSecurityRequirement + { + { + new OpenApiSecurityScheme + { + Reference = new OpenApiReference + { + Id = "Fyp-API-Key", + Type = ReferenceType.SecurityScheme + } + }, + new List<string>() + } + }); + + if (roles != null) operation.Description += "Required Roles: " + string.Join(", ", roles); + } + } +} \ No newline at end of file diff --git a/Helpers/ResponseOperationFilter.cs b/Helpers/ResponseOperationFilter.cs deleted file mode 100644 index 5ef82cedf2030e46f4618ea5d36c33773c641114..0000000000000000000000000000000000000000 --- a/Helpers/ResponseOperationFilter.cs +++ /dev/null @@ -1,5 +0,0 @@ -namespace Group17profile.helpers; - -public class ResponseOperationFilter -{ -} \ No newline at end of file diff --git a/Middleware/ExceptionMiddleware.cs b/Middleware/ExceptionMiddleware.cs index 76f8fe5d3591918df4cebb9573e6ebff19e943af..e39f156e15f11a5710b042a630f0f2549c2e71de 100644 --- a/Middleware/ExceptionMiddleware.cs +++ b/Middleware/ExceptionMiddleware.cs @@ -2,7 +2,6 @@ using System.Net; using Exceptions; -using Microsoft.EntityFrameworkCore; using Newtonsoft.Json; public class ExceptionMiddleware @@ -49,13 +48,6 @@ public class ExceptionMiddleware logger.LogWarning(ex.Message); break; - 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; - logger.LogError(ex.Message); - break; - default: envelope = new ResponseEnvelope<object>(ex); context.Response.StatusCode = (int) HttpStatusCode.InternalServerError; diff --git a/Middleware/SwaggerProfileExtensions.cs b/Middleware/SwaggerAuthExtensions.cs similarity index 57% rename from Middleware/SwaggerProfileExtensions.cs rename to Middleware/SwaggerAuthExtensions.cs index af1efe15dfebbdccbec2eaf1dd2ab4c87f8761d7..855f2f66f2106efb7ecb5323908dfe9b195ddcc9 100644 --- a/Middleware/SwaggerProfileExtensions.cs +++ b/Middleware/SwaggerAuthExtensions.cs @@ -1,9 +1,9 @@ namespace Group17profile.Middleware; -public static class SwaggerProfileExtensions +public static class SwaggerAuthExtensions { public static IApplicationBuilder UseSwaggerAuthorized(this IApplicationBuilder builder) { - return builder.UseMiddleware<SwaggerProfileMiddleware>(); + return builder.UseMiddleware<SwaggerAuthMiddleware>(); } } \ No newline at end of file diff --git a/Middleware/SwaggerProfileMiddleware.cs b/Middleware/SwaggerAuthMiddleware.cs similarity index 95% rename from Middleware/SwaggerProfileMiddleware.cs rename to Middleware/SwaggerAuthMiddleware.cs index ed687a58aa2fb4d158bb20ff23b5b4da0bb44bf3..7e540672c811ca6c24f366c276aaa921b6701348 100644 --- a/Middleware/SwaggerProfileMiddleware.cs +++ b/Middleware/SwaggerAuthMiddleware.cs @@ -3,11 +3,11 @@ using System.Net; using System.Text; -public class SwaggerProfileMiddleware +public class SwaggerAuthMiddleware { private readonly RequestDelegate _next; - public SwaggerProfileMiddleware(RequestDelegate next) + public SwaggerAuthMiddleware(RequestDelegate next) { _next = next; } diff --git a/Migrations/20230325181401_InitialMigration.Designer.cs b/Migrations/20230325181401_InitialMigration.Designer.cs deleted file mode 100644 index 13772b7689d6017808d24193aa532972dbfe0ed2..0000000000000000000000000000000000000000 --- a/Migrations/20230325181401_InitialMigration.Designer.cs +++ /dev/null @@ -1,50 +0,0 @@ -// <auto-generated /> -using System; -using Group17profile.Models; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Metadata; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; - -#nullable disable - -namespace Group17profile.Migrations -{ - [DbContext(typeof(ProfileDbContext))] - [Migration("20230325181401_InitialMigration")] - partial class InitialMigration - { - /// <inheritdoc /> - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder - .HasAnnotation("ProductVersion", "8.0.0-preview.2.23128.3") - .HasAnnotation("Relational:MaxIdentifierLength", 128); - - SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); - - modelBuilder.Entity("Group17profile.Models.Entities.Profile", b => - { - b.Property<Guid>("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uniqueidentifier"); - - b.Property<DateTimeOffset>("CreatedAt") - .HasColumnType("datetimeoffset"); - - b.Property<DateTimeOffset?>("DeletedAt") - .HasColumnType("datetimeoffset"); - - b.Property<string>("Name") - .HasColumnType("nvarchar(max)"); - - b.HasKey("Id"); - - b.ToTable("Profile"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/Migrations/20230325181401_InitialMigration.cs b/Migrations/20230325181401_InitialMigration.cs deleted file mode 100644 index baa35761d912e39ce981b9649476b13ca1405a4b..0000000000000000000000000000000000000000 --- a/Migrations/20230325181401_InitialMigration.cs +++ /dev/null @@ -1,36 +0,0 @@ -using System; -using Microsoft.EntityFrameworkCore.Migrations; - -#nullable disable - -namespace Group17profile.Migrations -{ - /// <inheritdoc /> - public partial class InitialMigration : Migration - { - /// <inheritdoc /> - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.CreateTable( - name: "Profile", - columns: table => new - { - Id = table.Column<Guid>(type: "uniqueidentifier", nullable: false), - Name = table.Column<string>(type: "nvarchar(max)", nullable: true), - CreatedAt = table.Column<DateTimeOffset>(type: "datetimeoffset", nullable: false), - DeletedAt = table.Column<DateTimeOffset>(type: "datetimeoffset", nullable: true) - }, - constraints: table => - { - table.PrimaryKey("PK_Profile", x => x.Id); - }); - } - - /// <inheritdoc /> - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropTable( - name: "Profile"); - } - } -} diff --git a/Migrations/20230407120441_AddedUser.Designer.cs b/Migrations/20230407120441_AddedUser.Designer.cs deleted file mode 100644 index d9093103d7da02fa35cc494ee6f7953674fb68b9..0000000000000000000000000000000000000000 --- a/Migrations/20230407120441_AddedUser.Designer.cs +++ /dev/null @@ -1,147 +0,0 @@ -// <auto-generated /> -using System; -using Group17profile.Models; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Metadata; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; - -#nullable disable - -namespace Group17profile.Migrations -{ - [DbContext(typeof(ProfileDbContext))] - [Migration("20230407120441_AddedUser")] - partial class AddedUser - { - /// <inheritdoc /> - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder - .HasAnnotation("ProductVersion", "8.0.0-preview.2.23128.3") - .HasAnnotation("Relational:MaxIdentifierLength", 128); - - SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); - - modelBuilder.Entity("Group17profile.Models.Entities.Profile", b => - { - b.Property<Guid>("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uniqueidentifier"); - - b.Property<string>("Banner") - .IsRequired() - .HasColumnType("nvarchar(max)"); - - b.Property<string>("Biography") - .IsRequired() - .HasColumnType("nvarchar(max)"); - - b.Property<DateTimeOffset>("CreatedAt") - .HasColumnType("datetimeoffset"); - - b.Property<DateTime>("DateCreated") - .HasColumnType("datetime2"); - - b.Property<DateTimeOffset?>("DeletedAt") - .HasColumnType("datetimeoffset"); - - b.Property<DateOnly>("DoB") - .HasColumnType("date"); - - b.Property<string>("Email") - .HasColumnType("nvarchar(max)"); - - b.Property<string>("Gender") - .IsRequired() - .HasColumnType("nvarchar(max)"); - - b.Property<string>("Name") - .HasColumnType("nvarchar(max)"); - - b.Property<string>("Password") - .HasColumnType("nvarchar(max)"); - - b.Property<DateTimeOffset?>("UpdatedAt") - .HasColumnType("datetimeoffset"); - - b.HasKey("Id"); - - b.ToTable("Profile"); - }); - - modelBuilder.Entity("Group17profile.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(max)"); - - 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<Guid?>("ProfileId") - .HasColumnType("uniqueidentifier"); - - b.Property<string>("ProfilePictureUrl") - .HasColumnType("nvarchar(max)"); - - 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("ProfileId"); - - b.ToTable("User"); - }); - - modelBuilder.Entity("Group17profile.Models.Entities.User", b => - { - b.HasOne("Group17profile.Models.Entities.Profile", "Profile") - .WithMany() - .HasForeignKey("ProfileId"); - - b.Navigation("Profile"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/Migrations/20230407120441_AddedUser.cs b/Migrations/20230407120441_AddedUser.cs deleted file mode 100644 index ea345e9e817905bb9a56dd476a8f7f965df41a73..0000000000000000000000000000000000000000 --- a/Migrations/20230407120441_AddedUser.cs +++ /dev/null @@ -1,143 +0,0 @@ -using System; -using Microsoft.EntityFrameworkCore.Migrations; - -#nullable disable - -namespace Group17profile.Migrations -{ - /// <inheritdoc /> - public partial class AddedUser : Migration - { - /// <inheritdoc /> - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.AddColumn<string>( - name: "Banner", - table: "Profile", - type: "nvarchar(max)", - nullable: false, - defaultValue: ""); - - migrationBuilder.AddColumn<string>( - name: "Biography", - table: "Profile", - type: "nvarchar(max)", - nullable: false, - defaultValue: ""); - - migrationBuilder.AddColumn<DateTime>( - name: "DateCreated", - table: "Profile", - type: "datetime2", - nullable: false, - defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified)); - - migrationBuilder.AddColumn<DateOnly>( - name: "DoB", - table: "Profile", - type: "date", - nullable: false, - defaultValue: new DateOnly(1, 1, 1)); - - migrationBuilder.AddColumn<string>( - name: "Email", - table: "Profile", - type: "nvarchar(max)", - nullable: true); - - migrationBuilder.AddColumn<string>( - name: "Gender", - table: "Profile", - type: "nvarchar(max)", - nullable: false, - defaultValue: ""); - - migrationBuilder.AddColumn<string>( - name: "Password", - table: "Profile", - type: "nvarchar(max)", - nullable: true); - - migrationBuilder.AddColumn<DateTimeOffset>( - name: "UpdatedAt", - table: "Profile", - type: "datetimeoffset", - nullable: true); - - migrationBuilder.CreateTable( - name: "User", - columns: table => new - { - Id = table.Column<int>(type: "int", nullable: false) - .Annotation("SqlServer:Identity", "1, 1"), - FirstName = table.Column<string>(type: "nvarchar(max)", nullable: false), - Surname = table.Column<string>(type: "nvarchar(max)", nullable: false), - EmailAddress = table.Column<string>(type: "nvarchar(max)", nullable: false), - Password = table.Column<string>(type: "nvarchar(max)", nullable: false), - LastLogin = table.Column<DateTimeOffset>(type: "datetimeoffset", nullable: true), - LastRefreshTokenIssued = table.Column<DateTimeOffset>(type: "datetimeoffset", nullable: true), - ProfilePictureUrl = table.Column<string>(type: "nvarchar(max)", nullable: true), - UnconfirmedEmail = table.Column<string>(type: "nvarchar(max)", nullable: true), - PasswordResetToken = table.Column<Guid>(type: "uniqueidentifier", nullable: true), - EmailConfirmationToken = table.Column<Guid>(type: "uniqueidentifier", nullable: true), - ProfileId = table.Column<Guid>(type: "uniqueidentifier", nullable: true), - CreatedAt = table.Column<DateTimeOffset>(type: "datetimeoffset", nullable: false), - DeletedAt = table.Column<DateTimeOffset>(type: "datetimeoffset", nullable: true), - UpdatedAt = table.Column<DateTimeOffset>(type: "datetimeoffset", nullable: true) - }, - constraints: table => - { - table.PrimaryKey("PK_User", x => x.Id); - table.ForeignKey( - name: "FK_User_Profile_ProfileId", - column: x => x.ProfileId, - principalTable: "Profile", - principalColumn: "Id"); - }); - - migrationBuilder.CreateIndex( - name: "IX_User_ProfileId", - table: "User", - column: "ProfileId"); - } - - /// <inheritdoc /> - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropTable( - name: "User"); - - migrationBuilder.DropColumn( - name: "Banner", - table: "Profile"); - - migrationBuilder.DropColumn( - name: "Biography", - table: "Profile"); - - migrationBuilder.DropColumn( - name: "DateCreated", - table: "Profile"); - - migrationBuilder.DropColumn( - name: "DoB", - table: "Profile"); - - migrationBuilder.DropColumn( - name: "Email", - table: "Profile"); - - migrationBuilder.DropColumn( - name: "Gender", - table: "Profile"); - - migrationBuilder.DropColumn( - name: "Password", - table: "Profile"); - - migrationBuilder.DropColumn( - name: "UpdatedAt", - table: "Profile"); - } - } -} diff --git a/Migrations/20230410142542_AddedProfile.Designer.cs b/Migrations/20230410142542_AddedProfile.Designer.cs deleted file mode 100644 index 3b0f60126e9864791efc21599ff9e0d2ec6ddb8a..0000000000000000000000000000000000000000 --- a/Migrations/20230410142542_AddedProfile.Designer.cs +++ /dev/null @@ -1,138 +0,0 @@ -// <auto-generated /> -using System; -using Group17profile.Models; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Metadata; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; - -#nullable disable - -namespace Group17profile.Migrations -{ - [DbContext(typeof(ProfileDbContext))] - [Migration("20230410142542_AddedProfile")] - partial class AddedProfile - { - /// <inheritdoc /> - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder - .HasAnnotation("ProductVersion", "8.0.0-preview.2.23128.3") - .HasAnnotation("Relational:MaxIdentifierLength", 128); - - SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); - - modelBuilder.Entity("Group17profile.Models.Entities.Profile", b => - { - b.Property<Guid>("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uniqueidentifier"); - - b.Property<string>("BannerUrl") - .HasColumnType("nvarchar(max)"); - - b.Property<string>("Biography") - .HasColumnType("nvarchar(max)"); - - b.Property<DateTimeOffset>("CreatedAt") - .HasColumnType("datetimeoffset"); - - b.Property<DateTimeOffset?>("DeletedAt") - .HasColumnType("datetimeoffset"); - - b.Property<DateTimeOffset?>("DoB") - .HasColumnType("datetimeoffset"); - - b.Property<string>("FavouriteShows") - .HasColumnType("nvarchar(max)"); - - b.Property<string>("Gender") - .HasColumnType("nvarchar(max)"); - - b.Property<string>("Pronoun") - .HasColumnType("nvarchar(max)"); - - b.Property<DateTimeOffset?>("UpdatedAt") - .HasColumnType("datetimeoffset"); - - b.HasKey("Id"); - - b.ToTable("Profile"); - }); - - modelBuilder.Entity("Group17profile.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(max)"); - - 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<Guid?>("ProfileId") - .HasColumnType("uniqueidentifier"); - - b.Property<string>("ProfilePictureUrl") - .HasColumnType("nvarchar(max)"); - - 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("ProfileId"); - - b.ToTable("User"); - }); - - modelBuilder.Entity("Group17profile.Models.Entities.User", b => - { - b.HasOne("Group17profile.Models.Entities.Profile", "Profile") - .WithMany() - .HasForeignKey("ProfileId"); - - b.Navigation("Profile"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/Migrations/20230410142542_AddedProfile.cs b/Migrations/20230410142542_AddedProfile.cs deleted file mode 100644 index 3552bf644f12c3b68ceebfe113be0eacb3823da6..0000000000000000000000000000000000000000 --- a/Migrations/20230410142542_AddedProfile.cs +++ /dev/null @@ -1,125 +0,0 @@ -using System; -using Microsoft.EntityFrameworkCore.Migrations; - -#nullable disable - -namespace Group17profile.Migrations -{ - /// <inheritdoc /> - public partial class AddedProfile : Migration - { - /// <inheritdoc /> - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropColumn( - name: "Banner", - table: "Profile"); - - migrationBuilder.DropColumn( - name: "DateCreated", - table: "Profile"); - - migrationBuilder.RenameColumn( - name: "Password", - table: "Profile", - newName: "Pronoun"); - - migrationBuilder.RenameColumn( - name: "Name", - table: "Profile", - newName: "FavouriteShows"); - - migrationBuilder.RenameColumn( - name: "Email", - table: "Profile", - newName: "BannerUrl"); - - migrationBuilder.AlterColumn<string>( - name: "Gender", - table: "Profile", - type: "nvarchar(max)", - nullable: true, - oldClrType: typeof(string), - oldType: "nvarchar(max)"); - - migrationBuilder.AlterColumn<DateTimeOffset>( - name: "DoB", - table: "Profile", - type: "datetimeoffset", - nullable: true, - oldClrType: typeof(DateOnly), - oldType: "date"); - - migrationBuilder.AlterColumn<string>( - name: "Biography", - table: "Profile", - type: "nvarchar(max)", - nullable: true, - oldClrType: typeof(string), - oldType: "nvarchar(max)"); - } - - /// <inheritdoc /> - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.RenameColumn( - name: "Pronoun", - table: "Profile", - newName: "Password"); - - migrationBuilder.RenameColumn( - name: "FavouriteShows", - table: "Profile", - newName: "Name"); - - migrationBuilder.RenameColumn( - name: "BannerUrl", - table: "Profile", - newName: "Email"); - - migrationBuilder.AlterColumn<string>( - name: "Gender", - table: "Profile", - type: "nvarchar(max)", - nullable: false, - defaultValue: "", - oldClrType: typeof(string), - oldType: "nvarchar(max)", - oldNullable: true); - - migrationBuilder.AlterColumn<DateOnly>( - name: "DoB", - table: "Profile", - type: "date", - nullable: false, - defaultValue: new DateOnly(1, 1, 1), - oldClrType: typeof(DateTimeOffset), - oldType: "datetimeoffset", - oldNullable: true); - - migrationBuilder.AlterColumn<string>( - name: "Biography", - table: "Profile", - type: "nvarchar(max)", - nullable: false, - defaultValue: "", - oldClrType: typeof(string), - oldType: "nvarchar(max)", - oldNullable: true); - - migrationBuilder.AddColumn<string>( - name: "Banner", - table: "Profile", - type: "nvarchar(max)", - nullable: false, - defaultValue: ""); - - migrationBuilder.AddColumn<DateTime>( - name: "DateCreated", - table: "Profile", - type: "datetime2", - nullable: false, - defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified)); - } - } -} diff --git a/Migrations/20230410154750_FixedProfile.Designer.cs b/Migrations/20230410154750_FixedProfile.Designer.cs deleted file mode 100644 index 9678b3eb9542c25c49122682c1bb04bc0f77f43f..0000000000000000000000000000000000000000 --- a/Migrations/20230410154750_FixedProfile.Designer.cs +++ /dev/null @@ -1,139 +0,0 @@ -// <auto-generated /> -using System; -using Group17profile.Models; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Metadata; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; - -#nullable disable - -namespace Group17profile.Migrations -{ - [DbContext(typeof(ProfileDbContext))] - [Migration("20230410154750_FixedProfile")] - partial class FixedProfile - { - /// <inheritdoc /> - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder - .HasAnnotation("ProductVersion", "8.0.0-preview.2.23128.3") - .HasAnnotation("Relational:MaxIdentifierLength", 128); - - SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); - - modelBuilder.Entity("Group17profile.Models.Entities.Profile", b => - { - b.Property<Guid>("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uniqueidentifier") - .HasDefaultValueSql("newid()"); - - b.Property<string>("BannerUrl") - .HasColumnType("nvarchar(max)"); - - b.Property<string>("Biography") - .HasColumnType("nvarchar(max)"); - - b.Property<DateTimeOffset>("CreatedAt") - .HasColumnType("datetimeoffset"); - - b.Property<DateTimeOffset?>("DeletedAt") - .HasColumnType("datetimeoffset"); - - b.Property<DateTimeOffset?>("DoB") - .HasColumnType("datetimeoffset"); - - b.Property<string>("FavouriteShows") - .HasColumnType("nvarchar(max)"); - - b.Property<string>("Gender") - .HasColumnType("nvarchar(max)"); - - b.Property<string>("Pronoun") - .HasColumnType("nvarchar(max)"); - - b.Property<DateTimeOffset?>("UpdatedAt") - .HasColumnType("datetimeoffset"); - - b.HasKey("Id"); - - b.ToTable("Profile"); - }); - - modelBuilder.Entity("Group17profile.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(max)"); - - 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<Guid?>("ProfileId") - .HasColumnType("uniqueidentifier"); - - b.Property<string>("ProfilePictureUrl") - .HasColumnType("nvarchar(max)"); - - 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("ProfileId"); - - b.ToTable("User"); - }); - - modelBuilder.Entity("Group17profile.Models.Entities.User", b => - { - b.HasOne("Group17profile.Models.Entities.Profile", "Profile") - .WithMany() - .HasForeignKey("ProfileId"); - - b.Navigation("Profile"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/Migrations/20230410154750_FixedProfile.cs b/Migrations/20230410154750_FixedProfile.cs deleted file mode 100644 index 1b1cd23903b04de4d4a1e72515f88ddd361e1c3c..0000000000000000000000000000000000000000 --- a/Migrations/20230410154750_FixedProfile.cs +++ /dev/null @@ -1,37 +0,0 @@ -using System; -using Microsoft.EntityFrameworkCore.Migrations; - -#nullable disable - -namespace Group17profile.Migrations -{ - /// <inheritdoc /> - public partial class FixedProfile : Migration - { - /// <inheritdoc /> - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.AlterColumn<Guid>( - name: "Id", - table: "Profile", - type: "uniqueidentifier", - nullable: false, - defaultValueSql: "newid()", - oldClrType: typeof(Guid), - oldType: "uniqueidentifier"); - } - - /// <inheritdoc /> - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.AlterColumn<Guid>( - name: "Id", - table: "Profile", - type: "uniqueidentifier", - nullable: false, - oldClrType: typeof(Guid), - oldType: "uniqueidentifier", - oldDefaultValueSql: "newid()"); - } - } -} diff --git a/Migrations/20230410162129_BetterProfile.Designer.cs b/Migrations/20230410162129_BetterProfile.Designer.cs deleted file mode 100644 index 9745b6ca8ec4af79b9c6f4c0999d4eb235bc0624..0000000000000000000000000000000000000000 --- a/Migrations/20230410162129_BetterProfile.Designer.cs +++ /dev/null @@ -1,142 +0,0 @@ -// <auto-generated /> -using System; -using Group17profile.Models; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Metadata; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; - -#nullable disable - -namespace Group17profile.Migrations -{ - [DbContext(typeof(ProfileDbContext))] - [Migration("20230410162129_BetterProfile")] - partial class BetterProfile - { - /// <inheritdoc /> - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder - .HasAnnotation("ProductVersion", "8.0.0-preview.2.23128.3") - .HasAnnotation("Relational:MaxIdentifierLength", 128); - - SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); - - modelBuilder.Entity("Group17profile.Models.Entities.Profile", b => - { - b.Property<Guid>("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uniqueidentifier") - .HasDefaultValueSql("newid()"); - - b.Property<string>("BannerUrl") - .HasColumnType("nvarchar(max)"); - - b.Property<string>("Biography") - .HasColumnType("nvarchar(max)"); - - b.Property<DateTimeOffset>("CreatedAt") - .HasColumnType("datetimeoffset"); - - b.Property<DateTimeOffset?>("DeletedAt") - .HasColumnType("datetimeoffset"); - - b.Property<DateTimeOffset?>("DoB") - .HasColumnType("datetimeoffset"); - - b.Property<string>("FavouriteShows") - .HasColumnType("nvarchar(max)"); - - b.Property<string>("Gender") - .HasColumnType("nvarchar(max)"); - - b.Property<string>("Pronoun") - .HasColumnType("nvarchar(max)"); - - b.Property<DateTimeOffset?>("UpdatedAt") - .HasColumnType("datetimeoffset"); - - b.HasKey("Id"); - - b.ToTable("Profile"); - }); - - modelBuilder.Entity("Group17profile.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<Guid?>("ProfileId") - .HasColumnType("uniqueidentifier"); - - b.Property<string>("ProfilePictureUrl") - .HasColumnType("nvarchar(max)"); - - 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.HasIndex("ProfileId"); - - b.ToTable("User"); - }); - - modelBuilder.Entity("Group17profile.Models.Entities.User", b => - { - b.HasOne("Group17profile.Models.Entities.Profile", "Profile") - .WithMany() - .HasForeignKey("ProfileId"); - - b.Navigation("Profile"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/Migrations/20230410162129_BetterProfile.cs b/Migrations/20230410162129_BetterProfile.cs deleted file mode 100644 index 9abebf0bd91a9f1eb120b1e6e12c1ee2281cffb8..0000000000000000000000000000000000000000 --- a/Migrations/20230410162129_BetterProfile.cs +++ /dev/null @@ -1,44 +0,0 @@ -using Microsoft.EntityFrameworkCore.Migrations; - -#nullable disable - -namespace Group17profile.Migrations -{ - /// <inheritdoc /> - public partial class BetterProfile : Migration - { - /// <inheritdoc /> - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.AlterColumn<string>( - name: "EmailAddress", - table: "User", - type: "nvarchar(450)", - nullable: false, - oldClrType: typeof(string), - oldType: "nvarchar(max)"); - - migrationBuilder.CreateIndex( - name: "IX_User_EmailAddress", - table: "User", - column: "EmailAddress", - unique: true); - } - - /// <inheritdoc /> - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropIndex( - name: "IX_User_EmailAddress", - table: "User"); - - migrationBuilder.AlterColumn<string>( - name: "EmailAddress", - table: "User", - type: "nvarchar(max)", - nullable: false, - oldClrType: typeof(string), - oldType: "nvarchar(450)"); - } - } -} diff --git a/Migrations/ProfileDbContextModelSnapshot.cs b/Migrations/ProfileDbContextModelSnapshot.cs deleted file mode 100644 index 8dc5b49c66b4178ba4d1259c2f240340222054d5..0000000000000000000000000000000000000000 --- a/Migrations/ProfileDbContextModelSnapshot.cs +++ /dev/null @@ -1,139 +0,0 @@ -// <auto-generated /> -using System; -using Group17profile.Models; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Metadata; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; - -#nullable disable - -namespace Group17profile.Migrations -{ - [DbContext(typeof(ProfileDbContext))] - partial class ProfileDbContextModelSnapshot : ModelSnapshot - { - protected override void BuildModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder - .HasAnnotation("ProductVersion", "8.0.0-preview.2.23128.3") - .HasAnnotation("Relational:MaxIdentifierLength", 128); - - SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); - - modelBuilder.Entity("Group17profile.Models.Entities.Profile", b => - { - b.Property<Guid>("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uniqueidentifier") - .HasDefaultValueSql("newid()"); - - b.Property<string>("BannerUrl") - .HasColumnType("nvarchar(max)"); - - b.Property<string>("Biography") - .HasColumnType("nvarchar(max)"); - - b.Property<DateTimeOffset>("CreatedAt") - .HasColumnType("datetimeoffset"); - - b.Property<DateTimeOffset?>("DeletedAt") - .HasColumnType("datetimeoffset"); - - b.Property<DateTimeOffset?>("DoB") - .HasColumnType("datetimeoffset"); - - b.Property<string>("FavouriteShows") - .HasColumnType("nvarchar(max)"); - - b.Property<string>("Gender") - .HasColumnType("nvarchar(max)"); - - b.Property<string>("Pronoun") - .HasColumnType("nvarchar(max)"); - - b.Property<DateTimeOffset?>("UpdatedAt") - .HasColumnType("datetimeoffset"); - - b.HasKey("Id"); - - b.ToTable("Profile"); - }); - - modelBuilder.Entity("Group17profile.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<Guid?>("ProfileId") - .HasColumnType("uniqueidentifier"); - - b.Property<string>("ProfilePictureUrl") - .HasColumnType("nvarchar(max)"); - - 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.HasIndex("ProfileId"); - - b.ToTable("User"); - }); - - modelBuilder.Entity("Group17profile.Models.Entities.User", b => - { - b.HasOne("Group17profile.Models.Entities.Profile", "Profile") - .WithMany() - .HasForeignKey("ProfileId"); - - b.Navigation("Profile"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/Models/DTOs/ProfileDTO.cs b/Models/DTOs/ProfileDTO.cs index ff8dfecbfd9e64fb310d3a7992a06cc9d57ddd7e..cc4627e6320b7a4bf96a596ece65667e6081e77f 100644 --- a/Models/DTOs/ProfileDTO.cs +++ b/Models/DTOs/ProfileDTO.cs @@ -4,6 +4,7 @@ using DefaultObjects; public class ProfileDTO : DefaultGuidDTO { + public int? UserId { get; set; } public string? Biography { get; set; } public string? BannerUrl { get; set; } diff --git a/Models/DTOs/UserDTO.cs b/Models/DTOs/UserDTO.cs deleted file mode 100644 index d8e2941ac9e277b51137f9dc8e0bf63eb7eccb22..0000000000000000000000000000000000000000 --- a/Models/DTOs/UserDTO.cs +++ /dev/null @@ -1,33 +0,0 @@ -namespace Group17profile.Models.DTOs; - -using System.ComponentModel.DataAnnotations; -using DefaultObjects; - -public class UserDTO : DefaultIntDTO -{ - [Required] public string? FirstName { get; set; } - - [Required] public string? Surname { get; set; } - - public string Username => $"{FirstName} {Surname}"; - - [Required] public string? EmailAddress { get; set; } - - public DateTimeOffset? LastLogin { get; set; } - public DateTimeOffset? LastRefreshTokenIssued { get; set; } - - public string? ProfilePictureUrl { get; set; } - - public string? UnconfirmedEmail { get; set; } -} - -public class UserProfileDTO -{ - public UserDTO? User { get; set; } - public ProfileDTO? Profile { get; set; } -} - -public class RefreshTokenRequestDTO -{ - public string? RefreshToken { get; set; } -} \ No newline at end of file diff --git a/Models/DefaultObjects/DefaultEntity.cs b/Models/DefaultObjects/DefaultEntity.cs index eed48b4124db49c9a20a64a166105d4155a9df8f..03b0369ad0f575ab4a64b20f0c688aca623f11d9 100644 --- a/Models/DefaultObjects/DefaultEntity.cs +++ b/Models/DefaultObjects/DefaultEntity.cs @@ -1,7 +1,7 @@ namespace Group17profile.Models.DefaultObjects; -using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; +using Newtonsoft.Json; public class DefaultGuidEntity : IDefaultEntity, IGuidId, ITrackable { @@ -9,9 +9,7 @@ public class DefaultGuidEntity : IDefaultEntity, IGuidId, ITrackable public DateTimeOffset? DeletedAt { get; set; } - [Key] - [DatabaseGenerated(DatabaseGeneratedOption.Identity)] - public Guid Id { get; set; } + [JsonProperty(PropertyName = "id")] public Guid Id { get; set; } public DateTimeOffset? UpdatedAt { get; set; } } @@ -21,8 +19,7 @@ public class DefaultIntEntity : IDefaultEntity, IIntId, ITrackable public DateTimeOffset CreatedAt { get; set; } = DateTimeOffset.UtcNow; public DateTimeOffset? DeletedAt { get; set; } - [Key] - public int Id { get; set; } + [JsonProperty(PropertyName = "id")] public int Id { get; set; } public DateTimeOffset? UpdatedAt { get; set; } } diff --git a/Models/Entities/Profile.cs b/Models/Entities/Profile.cs index 7dfbad30e12229291e29327ebc461487cbb44b6a..d49e7cd9f04640b41088533a766e9ab2a88b6d84 100644 --- a/Models/Entities/Profile.cs +++ b/Models/Entities/Profile.cs @@ -4,6 +4,7 @@ using DefaultObjects; public class Profile : DefaultGuidEntity { + public int? UserId { get; set; } public string? Biography { get; set; } public string? BannerUrl { get; set; } diff --git a/Models/Entities/ProfileDbSettings.cs b/Models/Entities/ProfileDbSettings.cs deleted file mode 100644 index b9aa4217bf5b374392cab875940cc426426e2b99..0000000000000000000000000000000000000000 --- a/Models/Entities/ProfileDbSettings.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace Group17profile.Models.Entities; - -public class ProfileDbSettings -{ - public string ConnectionString { get; set; } = null!; - - public string DatabaseName { get; set; } = null!; -} \ No newline at end of file diff --git a/Models/Entities/User.cs b/Models/Entities/User.cs deleted file mode 100644 index 586be2426a82aee41825a098ce1a54d9676c7999..0000000000000000000000000000000000000000 --- a/Models/Entities/User.cs +++ /dev/null @@ -1,33 +0,0 @@ -namespace Group17profile.Models.Entities; - -using System.ComponentModel.DataAnnotations; -using System.ComponentModel.DataAnnotations.Schema; -using DefaultObjects; - -public class User : DefaultIntEntity -{ - [Required] public string? FirstName { get; set; } - - [Required] public string? Surname { get; set; } - - [Required] public string? EmailAddress { get; set; } - - [Required] public string? Password { get; set; } - - 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; } - public Guid? EmailConfirmationToken { get; set; } - - [ForeignKey("Profile")] public Guid? ProfileId { get; set; } - - [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 2af10fcade4260f55d13eb34d8026f55bb5102fb..90c0b978fcf70d4af8651b7314d3d682db830502 100644 --- a/Models/ProfileDbContext.cs +++ b/Models/ProfileDbContext.cs @@ -1,77 +1,24 @@ namespace Group17profile.Models; -using System.ComponentModel.DataAnnotations.Schema; -using DefaultObjects; -using Entities; -using Microsoft.EntityFrameworkCore; +using Microsoft.Azure.Cosmos; +using Microsoft.Extensions.Options; +using Settings; -public class ProfileDbContext : DbContext +public class ProfileDbContext { - public static string ConnectionStringName = ""; + private readonly Container _container; - - public ProfileDbContext(DbContextOptions<ProfileDbContext> options) : base(options) - { - } - - public DbSet<Profile> Profile { get; set; } = null!; - public DbSet<User> User { get; set; } = null!; - - protected override void OnModelCreating(ModelBuilder modelBuilder) + public ProfileDbContext(IOptions<ConnectionStrings> connectionStrings) { - base.OnModelCreating(modelBuilder); - - modelBuilder.Entity<User>().HasIndex(u => u.EmailAddress).IsUnique(); - - foreach (var entity in modelBuilder.Model.GetEntityTypes().Where(t => - t.ClrType.GetProperties().Any(p => - p.CustomAttributes.Any(a => a.AttributeType == typeof(DatabaseGeneratedAttribute))))) + var clientOptions = new CosmosClientOptions { - foreach (var property in entity.ClrType.GetProperties().Where(p => - p.PropertyType == typeof(Guid) && p.CustomAttributes.Any(a => - a.AttributeType == typeof(DatabaseGeneratedAttribute)))) - modelBuilder.Entity(entity.ClrType).Property(property.Name).HasDefaultValueSql("newid()"); - foreach (var property in entity.ClrType.GetProperties().Where(p => - p.PropertyType == typeof(DateTimeOffset) && - p.CustomAttributes.Any(a => a.AttributeType == typeof(DatabaseGeneratedAttribute)))) - modelBuilder.Entity(entity.ClrType).Property(property.Name).HasDefaultValueSql("SYSDATETIMEOFFSET()"); - } - } - - public override int SaveChanges(bool acceptAllChangesOnSuccess) - { - UpdateTimestamps(); - return base.SaveChanges(acceptAllChangesOnSuccess); - } - - public override int SaveChanges() - { - UpdateTimestamps(); - return base.SaveChanges(); - } - - public override Task<int> SaveChangesAsync(bool acceptAllChangesOnSuccess, - CancellationToken cancellationToken = default) - { - UpdateTimestamps(); - return base.SaveChangesAsync(acceptAllChangesOnSuccess, cancellationToken); + RequestTimeout = TimeSpan.FromSeconds(30) + }; + var connectionString = connectionStrings.Value.DbConnectionString; + var client = new CosmosClient(connectionString, clientOptions); + var database = client.GetDatabase("ProfileDb"); + _container = database.GetContainer("Profile"); } - public override Task<int> SaveChangesAsync(CancellationToken cancellationToken = default) - { - UpdateTimestamps(); - return base.SaveChangesAsync(cancellationToken); - } - - private void UpdateTimestamps() - { - var trackableModified = ChangeTracker.Entries() - .Where(t => t.State == EntityState.Modified && t.Entity is ITrackable) - .Select(t => t.Entity) - .ToList(); - - foreach (var t in trackableModified) - if (t is ITrackable trackable) - trackable.UpdatedAt = DateTimeOffset.UtcNow; - } + public Container? Profile => _container; } \ No newline at end of file diff --git a/Program.cs b/Program.cs index ad97ce0c82f99ede419ab89a50718d75522340f7..116a1740413eb49e676a735903ed43e8437245a8 100644 --- a/Program.cs +++ b/Program.cs @@ -1,29 +1,94 @@ +using System.Security.Claims; +using System.Text; using AutoMapper; using Group17profile; +using Group17profile.helpers; +using Group17profile.Middleware; using Group17profile.Models; using Group17profile.Repositories; using Group17profile.Services; using Group17profile.Settings; -using Microsoft.EntityFrameworkCore; +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.Extensions.Options; +using Microsoft.IdentityModel.Tokens; +using Microsoft.OpenApi.Models; +using Swashbuckle.AspNetCore.SwaggerUI; var builder = WebApplication.CreateBuilder(args); // Add services to the container. - builder.Services.AddControllers(); +builder.Services.AddOptions(); builder.Configuration.AddJsonFile("appsettings.Development.json", true, true); -builder.Services.AddDbContext<ProfileDbContext>( - opt => opt.UseSqlServer(builder.Configuration.GetConnectionString("DbConnectionString"))); +builder.Configuration.AddEnvironmentVariables(); +builder.Services.Configure<TokenSettings>(builder.Configuration.GetSection("TokenSettings")); +builder.Services.Configure<FrontendStrings>(builder.Configuration.GetSection("FrontendStrings")); builder.Services.Configure<ConnectionStrings>(builder.Configuration.GetSection("ConnectionStrings")); - -// Automapper -var mapperConfig = new MapperConfiguration(mc => { mc.AddProfile(new AutoMapperProfile()); }); -var mapper = mapperConfig.CreateMapper(); -builder.Services.AddSingleton(mapper); - +builder.Services.AddSingleton(c => new ProfileDbContext(c.GetRequiredService<IOptions<ConnectionStrings>>())); +builder.Services.AddSwaggerGen(c => +{ + c.SwaggerDoc("v1", new OpenApiInfo {Title = "Group17ProfileMicroservice", Version = "v1"}); +}); +var tokenSettings = builder.Configuration.GetSection("TokenSettings").Get<TokenSettings>(); +var key = Encoding.ASCII.GetBytes(tokenSettings!.Secret); // Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle builder.Services.AddEndpointsApiExplorer(); -builder.Services.AddSwaggerGen(); +builder.Services.AddSwaggerGen(g => +{ + g.OperationFilter<AuthResponsesOperationFilter>(); + g.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme + { + Description = "JWT Authorization header using the Bearer scheme.", + Name = "Authorization", + In = ParameterLocation.Header, + Type = SecuritySchemeType.Http, + Scheme = "Bearer" + }); + g.AddSecurityRequirement(new OpenApiSecurityRequirement + { + { + new OpenApiSecurityScheme + {Reference = new OpenApiReference {Type = ReferenceType.SecurityScheme, Id = "Bearer"}}, + Array.Empty<string>() + } + }); + g.UseAllOfToExtendReferenceSchemas(); +}); +builder.Services.AddSwaggerGenNewtonsoftSupport(); +builder.Services.AddHttpClient(); +builder.Services.AddHttpContextAccessor(); +builder.Services.AddOutputCache(); +builder.Services.AddAuthentication(options => { options.DefaultScheme = "smart"; }).AddJwtBearer(j => +{ + j.RequireHttpsMetadata = false; + j.SaveToken = true; + j.TokenValidationParameters = new TokenValidationParameters + { + ValidateIssuerSigningKey = true, + IssuerSigningKey = new SymmetricSecurityKey(key), + ValidateIssuer = false, + ValidateAudience = false, + ValidateLifetime = true, + ClockSkew = TimeSpan.Zero + }; + j.Events = new JwtBearerEvents + { + OnMessageReceived = context => + { + var accessToken = context.Request.Query["access_token"]; + + if (!string.IsNullOrEmpty(accessToken)) context.Token = accessToken; + + return Task.CompletedTask; + } + }; +}).AddPolicyScheme("smart", "Authorization", + options => { options.ForwardDefaultSelector = _ => JwtBearerDefaults.AuthenticationScheme; }); + +builder.Services.AddAuthorization(options => +{ + options.AddPolicy(Constants.AuthorizationPolicies.HasUserId, policy => policy.RequireClaim(ClaimTypes.Sid)); +}); builder.Services.AddCors(options => { @@ -37,8 +102,13 @@ builder.Services.AddCors(options => }); }); +// Automapper +var mapperConfig = new MapperConfiguration(mc => { mc.AddProfile(new AutoMapperProfile()); }); +var mapper = mapperConfig.CreateMapper(); +builder.Services.AddSingleton(mapper); + // Add repositories in /Repositories -builder.Services.AddTransient(typeof(IBaseRepository<>), typeof(BaseRepository<>)); +builder.Services.AddTransient<IProfileRepository, ProfileRepository>(); // Add services in /Services builder.Services.AddTransient<IProfileService, ProfileService>(); @@ -46,6 +116,12 @@ builder.Services.AddTransient<IStorageService, StorageService>(); var app = builder.Build(); +var swaggerOptions = new SwaggerUIOptions +{ + RoutePrefix = string.Empty +}; + + // Configure the HTTP request pipeline. if (app.Environment.IsDevelopment()) { @@ -53,12 +129,24 @@ if (app.Environment.IsDevelopment()) app.UseSwaggerUI(); } +app.MapControllers(); +app.UseStaticFiles(); +app.UseRouting(); app.UseCors(); - -app.UseHttpsRedirection(); - +app.UseAuthentication(); +app.UseSwagger(); app.UseAuthorization(); +app.UseSwaggerAuthorized(); +app.UseSwaggerUI(options => +{ + options.SwaggerEndpoint("/swagger/v1/swagger.json", "Group 17 Profile Microservice v1"); + options.RoutePrefix = string.Empty; + options.ConfigObject = swaggerOptions.ConfigObject; +}); +app.UseMiddleware<ExceptionMiddleware>(); +app.UseHttpsRedirection(); +app.UseOutputCache(); -app.MapControllers(); +app.UseEndpoints(endpoints => { endpoints.MapControllers(); }); app.Run(); \ No newline at end of file diff --git a/Repositories/BaseRepository.cs b/Repositories/BaseRepository.cs deleted file mode 100644 index 7be3fe2d47e8c6745848384a94ee4ff2c2fd7fc3..0000000000000000000000000000000000000000 --- a/Repositories/BaseRepository.cs +++ /dev/null @@ -1,119 +0,0 @@ -namespace Group17profile.Repositories; - -using System.Net; -using Exceptions; -using Microsoft.EntityFrameworkCore; -using Models; - -public interface IBaseRepository<TEntity> where TEntity : class -{ - TEntity? GetById(int id); - TEntity GetByIdThrowIfNull(int id); - TEntity? GetById(Guid id); - TEntity GetByIdThrowIfNull(Guid id); - IQueryable<TEntity> GetAll(); - Task<TEntity> CreateAndSaveAsync(TEntity entity); - Task<TEntity> UpdateAndSaveAsync(TEntity entity); - Task DeleteAndSaveAsync(TEntity entity); - void PrepareToAdd(TEntity toAdd); - void PrepareToAdd(IEnumerable<TEntity> toAdd); - void PrepareToUpdate(TEntity toUpdate); - void PrepareToUpdate(IEnumerable<TEntity> toUpdate); - void PrepareToDelete(TEntity toDelete); - void PrepareToDelete(IEnumerable<TEntity> toDelete); - Task CommitPendingChanges(); -} - -public sealed class BaseRepository<TEntity> : IBaseRepository<TEntity> where TEntity : class -{ - private readonly ProfileDbContext _context; - private readonly DbSet<TEntity> _dbSet; - - public BaseRepository(ProfileDbContext context) - { - _context = context; - _dbSet = _context.Set<TEntity>(); - } - - public TEntity? GetById(int id) - { - return _dbSet.Find(id); - } - - public TEntity? GetById(Guid id) - { - return _dbSet.Find(id); - } - - public TEntity GetByIdThrowIfNull(int id) - { - return GetById(id) ?? throw new ProfileException($"Item with ID {id} does not exist.", HttpStatusCode.NotFound); - } - - public TEntity GetByIdThrowIfNull(Guid id) - { - return GetById(id) ?? throw new ProfileException($"Item with ID {id} does not exist.", HttpStatusCode.NotFound); - } - - public IQueryable<TEntity> GetAll() - { - return _dbSet.AsQueryable(); - } - - public async Task<TEntity> CreateAndSaveAsync(TEntity entity) - { - var created = await _dbSet.AddAsync(entity); - await _context.SaveChangesAsync(); - - return created.Entity; - } - - public async Task<TEntity> UpdateAndSaveAsync(TEntity entity) - { - var updated = _dbSet.Update(entity); - await _context.SaveChangesAsync(); - - return updated.Entity; - } - - public async Task DeleteAndSaveAsync(TEntity entity) - { - _dbSet.Remove(entity); - await _context.SaveChangesAsync(); - } - - public void PrepareToAdd(TEntity toAdd) - { - _dbSet.Add(toAdd); - } - - public void PrepareToAdd(IEnumerable<TEntity> toAdd) - { - _dbSet.AddRange(toAdd); - } - - public void PrepareToUpdate(TEntity toUpdate) - { - _dbSet.Update(toUpdate); - } - - public void PrepareToUpdate(IEnumerable<TEntity> toUpdate) - { - _dbSet.UpdateRange(toUpdate); - } - - public void PrepareToDelete(TEntity toDelete) - { - _dbSet.Remove(toDelete); - } - - public void PrepareToDelete(IEnumerable<TEntity> toDelete) - { - _dbSet.RemoveRange(toDelete); - } - - public async Task CommitPendingChanges() - { - await _context.SaveChangesAsync(); - } -} \ No newline at end of file diff --git a/Repositories/ProfileRepository.cs b/Repositories/ProfileRepository.cs new file mode 100644 index 0000000000000000000000000000000000000000..52ddf97be8406c2e602b6825f95dfaed8fec6b57 --- /dev/null +++ b/Repositories/ProfileRepository.cs @@ -0,0 +1,80 @@ +namespace Group17profile.Repositories; + +using System.Net; +using Microsoft.Azure.Cosmos; +using Models; +using Models.Entities; + +public interface IProfileRepository +{ + Task<Profile> CreateProfileAsync(Profile profile); + Task<Profile?> GetProfileAsync(int userId); + Task<Profile> UpdateProfileAsync(int userId, string profileId, Profile profile); + Task DeleteProfileAsync(int userId, string profileId); + Task<IEnumerable<Profile?>> GetProfilesByMoodAsync(string mood); +} + +public class ProfileRepository : IProfileRepository +{ + private readonly Container? _profileDetailsContainer; + + public ProfileRepository(ProfileDbContext context) + { + _profileDetailsContainer = context.Profile; + } + + public async Task<Profile> CreateProfileAsync(Profile profile) + { + var response = + await _profileDetailsContainer?.CreateItemAsync(profile, + new PartitionKey(profile.UserId.GetValueOrDefault()))!; + return response.Resource; + } + + public async Task<Profile?> GetProfileAsync(int userId) + { + try + { + var query = new QueryDefinition("SELECT * FROM c WHERE c.UserId = @UserId") + .WithParameter("@UserId", userId); + + var iterator = _profileDetailsContainer?.GetItemQueryIterator<Profile>(query); + + if (iterator is {HasMoreResults: false}) return null; + var response = await iterator.ReadNextAsync(); + return response.FirstOrDefault(); + } + catch (CosmosException ex) when (ex.StatusCode == HttpStatusCode.NotFound) + { + return null; + } + } + + public async Task<Profile> UpdateProfileAsync(int userId, string profileId, Profile profile) + { + var response = await _profileDetailsContainer.ReplaceItemAsync(profile, profileId, new PartitionKey(userId)); + return response.Resource; + } + + public async Task DeleteProfileAsync(int userId, string profileId) + { + await _profileDetailsContainer.DeleteItemAsync<Profile>(profileId, new PartitionKey(userId)); + } + + public async Task<IEnumerable<Profile?>> GetProfilesByMoodAsync(string mood) + { + var query = new QueryDefinition("SELECT * FROM c WHERE c.Mood = @mood") + .WithParameter("@mood", mood); + + var iterator = _profileDetailsContainer?.GetItemQueryIterator<Profile>(query); + + var profiles = new List<Profile>(); + while (iterator.HasMoreResults) + { + var response = await iterator.ReadNextAsync(); + profiles.AddRange(response); + } + + return profiles; + } +} \ No newline at end of file diff --git a/Services/ProfileService.cs b/Services/ProfileService.cs index 35390f4fbf8659d681ea4225f2e38a07ad19d6f6..3c741e3b524cf6ebb8a678fa2b63109c3077fe5a 100644 --- a/Services/ProfileService.cs +++ b/Services/ProfileService.cs @@ -3,50 +3,69 @@ using System.Text; using AutoMapper; using Exceptions; -using Microsoft.EntityFrameworkCore; -using Models; using Models.DTOs; -using Models.Entities; using Repositories; using Profile = Models.Entities.Profile; public interface IProfileService { + Task<ProfileDTO> GetProfileForUser(int userId); Task<ProfileDTO> CreateOrUpdateProfile(ProfileDTO profile, int userId); - Task<UserProfileDTO> GetProfileForUser(int userId); - Task<string> UploadBannerPicture(int userId, IFormFile bannerPicture); + Task<ProfileDTO> UploadBannerPicture(int userId, IFormFile bannerPicture); } public class ProfileService : IProfileService { private readonly IMapper _mapper; - private readonly IBaseRepository<Profile> _profileRepository; + private readonly IProfileRepository _profileRepository; private readonly IStorageService _storageService; - private readonly IBaseRepository<User> _userRepository; - public ProfileService(IMapper mapper, IBaseRepository<Profile> profileRepository, - IBaseRepository<User> userRepository, IStorageService storageService) + public ProfileService(IMapper mapper, IProfileRepository profileRepository, IStorageService storageService) { _mapper = mapper; _profileRepository = profileRepository; - _userRepository = userRepository; _storageService = storageService; } + public async Task<ProfileDTO> GetProfileForUser(int userId) + { + var profile = await _profileRepository.GetProfileAsync(userId); + if (profile == null) + throw new ProfileException("Profile does not exist."); + if (!string.IsNullOrWhiteSpace(profile.BannerUrl)) + profile.BannerUrl += + _storageService.GetSasForFile(Constants.AzureBlobContainer.BannerPictures, profile.BannerUrl); + var record = _mapper.Map<ProfileDTO>(profile); + var s = profile.FavouriteShows?.Split(","); + record.FavouriteShows = s?.ToList(); + + return record; + } + 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."); + if (!string.IsNullOrWhiteSpace(profile.BannerUrl)) + { + var bannerUri = new Uri(profile.BannerUrl); + if (!string.IsNullOrWhiteSpace(bannerUri.Query)) + profile.BannerUrl = profile.BannerUrl.Replace(bannerUri.Query, ""); + } var record = _mapper.Map<Profile>(profile); - if (user.ProfileId != null) + var existing = await _profileRepository.GetProfileAsync(userId); + if (existing == null) { - var existingProfile = await _profileRepository.GetAll().AsNoTracking() - .FirstOrDefaultAsync(p => p.Id == user.ProfileId); - record.Id = existingProfile!.Id; + record.UserId = userId; + record = await _profileRepository.CreateProfileAsync(record); + } + else + { + record.Id = existing.Id; + record.UserId = existing.UserId; + record = await _profileRepository.UpdateProfileAsync(userId, record.Id.ToString(), record); } if (profile.FavouriteShows != null && profile.FavouriteShows.Count != 0) @@ -58,59 +77,34 @@ public class ProfileService : IProfileService record.FavouriteShows = builder.ToString(); } - if (user.ProfileId == null) - { - var newProfile = await _profileRepository.CreateAndSaveAsync(record); - user.ProfileId = newProfile.Id; - await _userRepository.UpdateAndSaveAsync(user); - return _mapper.Map<ProfileDTO>(newProfile); - } + if (!string.IsNullOrWhiteSpace(record.BannerUrl)) + record.BannerUrl += + _storageService.GetSasForFile(Constants.AzureBlobContainer.BannerPictures, record.BannerUrl); - var updatedProfile = await _profileRepository.UpdateAndSaveAsync(record); - return _mapper.Map<ProfileDTO>(updatedProfile); + return _mapper.Map<ProfileDTO>(record); } - public async Task<UserProfileDTO> GetProfileForUser(int userId) + public async Task<ProfileDTO> UploadBannerPicture(int userId, IFormFile bannerPicture) { - var user = _userRepository.GetByIdThrowIfNull(userId); - if (user.ProfileId == null) - 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 async Task<string> UploadBannerPicture(int userId, IFormFile bannerPicture) - { - var user = _userRepository.GetByIdThrowIfNull(userId); - + var profile = await _profileRepository.GetProfileAsync(userId); using var ms = new MemoryStream(); await bannerPicture.CopyToAsync(ms); ms.Position = 0; - var fileName = $"{user.Id}/{user.FirstName}banner"; + var fileName = $"{userId}/{bannerPicture.Name}"; 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); - user.ProfileId = newProfile.Id; - await _userRepository.UpdateAndSaveAsync(user); - return _storageService.GetSasForFile(Constants.AzureBlobContainer.BannerPictures, image.ToString()); + profile = new Profile {BannerUrl = image.ToString(), UserId = userId}; + profile = await _profileRepository.CreateProfileAsync(profile); + } + else + { + profile.BannerUrl = image.ToString(); + profile = await _profileRepository.UpdateProfileAsync(userId, profile.Id.ToString(), profile); } - profile.BannerUrl = image.ToString(); - await _profileRepository.UpdateAndSaveAsync(profile); - return _storageService.GetSasForFile(Constants.AzureBlobContainer.BannerPictures, image.ToString()); + profile.BannerUrl += + _storageService.GetSasForFile(Constants.AzureBlobContainer.BannerPictures, image.ToString()); + return _mapper.Map<ProfileDTO>(profile); } } \ No newline at end of file diff --git a/Services/StorageService.cs b/Services/StorageService.cs index e5c387cd8a6ef0b73805f94bd4b5f67f91de1d38..34f9cf2e4f8592dc452945e9ac2280cddd539292 100644 --- a/Services/StorageService.cs +++ b/Services/StorageService.cs @@ -1,16 +1,15 @@ -using Azure.Storage.Blobs.Models; - -namespace Group17profile.Services; +namespace Group17profile.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 { - string GetSasForFile(string containerName, string url, DateTimeOffset? expires = null); + string? GetSasForFile(string containerName, string url, DateTimeOffset? expires = null); Task<Uri> SaveImageAsJpgBlob(string containerName, string fileName, Stream input, Dictionary<string, string>? metadata = null); @@ -25,15 +24,15 @@ public class StorageService : IStorageService _connectionStrings = connectionStrings.Value; } - public string GetSasForFile(string containerName, string url, DateTimeOffset? expires = null) + public string? GetSasForFile(string containerName, string url, DateTimeOffset? expires = null) { url = GetFileFromUrl(url, containerName) ?? string.Empty; if (string.IsNullOrWhiteSpace(url)) - return string.Empty; + return null; 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; + var urlWithSas = blob.GenerateSasUri(BlobSasPermissions.Read, DateTimeOffset.Now.AddHours(3)); + return urlWithSas.Query; } public async Task<Uri> SaveImageAsJpgBlob(string containerName, string fileName, Stream input, @@ -46,7 +45,7 @@ public class StorageService : IStorageService await clone.SaveAsJpegAsync(croppedImage); croppedImage.Position = 0; var blob = await GetBlobReference(containerName, fileName); - await blob.UploadAsync(croppedImage, new BlobHttpHeaders{ContentType = "image/jpeg"}); + await blob.UploadAsync(croppedImage, new BlobHttpHeaders {ContentType = "image/jpeg"}); return blob.Uri; } diff --git a/Settings/ConnectionStrings.cs b/Settings/ConnectionStrings.cs index 756e2537feb4bc079048c8e66cdd9c277fb58a90..0a688ac6e4a337a01bb7300e520dce122af61c75 100644 --- a/Settings/ConnectionStrings.cs +++ b/Settings/ConnectionStrings.cs @@ -1,10 +1,7 @@ -using Microsoft.Azure.Cosmos; - -namespace Group17profile.Settings; +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/Settings/FrontendStrings.cs b/Settings/FrontendStrings.cs new file mode 100644 index 0000000000000000000000000000000000000000..ca8d611b594ad31df2a5a1b13ff386525825c1dd --- /dev/null +++ b/Settings/FrontendStrings.cs @@ -0,0 +1,6 @@ +namespace Group17profile.Settings; + +public class FrontendStrings +{ + public string? BaseUrl { get; set; } +} \ No newline at end of file diff --git a/Settings/TokenSettings.cs b/Settings/TokenSettings.cs new file mode 100644 index 0000000000000000000000000000000000000000..4d918a05d8c7e884ab903aa09d35491506e5c816 --- /dev/null +++ b/Settings/TokenSettings.cs @@ -0,0 +1,10 @@ +namespace Group17profile.Settings; + +public class TokenSettings +{ + public int RefreshTokenValidityMinutes { get; set; } = 43200; + public int AccessTokenValidityMinutes { get; set; } = 60; + public int RefreshTokenRandomNumbers { get; set; } = 32; + public string Secret { get; set; } = string.Empty; + public bool AllowMultipleRefreshTokens { get; set; } = true; +} \ No newline at end of file