diff --git a/AdvancedAPI.Business/MappingProfile.cs b/AdvancedAPI.Business/MappingProfile.cs index a65aa77..fba7e4f 100644 --- a/AdvancedAPI.Business/MappingProfile.cs +++ b/AdvancedAPI.Business/MappingProfile.cs @@ -1,5 +1,6 @@ using AdvancedAPI.Data.Models; using AdvancedAPI.Data.ViewModels.NewsArticle; +using AdvancedAPI.Data.ViewModels.User; using AutoMapper; namespace Business; @@ -15,5 +16,7 @@ public class MappingProfile : Profile public MappingProfile() { CreateMap(); + CreateMap(MemberList.None) + .ForMember(d => d.Gender, opt => opt.MapFrom(s => s.Gender.Name)); } } diff --git a/AdvancedAPI.Business/ServiceExtension.cs b/AdvancedAPI.Business/ServiceExtension.cs index 8e629b8..45328e8 100644 --- a/AdvancedAPI.Business/ServiceExtension.cs +++ b/AdvancedAPI.Business/ServiceExtension.cs @@ -17,6 +17,7 @@ public static IServiceCollection AddBusinessServices(this IServiceCollection ser services.AddScoped(); services.AddScoped(); + services.AddScoped(); return services; } diff --git a/AdvancedAPI.Business/Services/AuthenticationService.cs b/AdvancedAPI.Business/Services/AuthenticationService.cs index fcdd9ac..eaa7f9e 100644 --- a/AdvancedAPI.Business/Services/AuthenticationService.cs +++ b/AdvancedAPI.Business/Services/AuthenticationService.cs @@ -1,10 +1,10 @@ using System.IdentityModel.Tokens.Jwt; using System.Security.Claims; using System.Text; +using AdvancedAPI.Data.Models; using AdvancedAPI.Data.Repositories.Interfaces; using AdvancedAPI.Data.ViewModels.Authentication; using Business.Services.Interfaces; -using Microsoft.AspNetCore.Identity; using Microsoft.IdentityModel.Tokens; namespace Business.Services; @@ -27,12 +27,12 @@ public AuthenticationService(IIdentityRepository identityRepository, IConfigurat /// public async Task Login(LoginRequestModel requestModel, CancellationToken ct = default) { - IdentityUser? user = await _identityRepository.GetUser(requestModel.Username); + User? user = await _identityRepository.GetUserByName(requestModel.Username); if (user != null && await _identityRepository.CheckPassword(user, requestModel.Password)) { List authClaims = new() { - new Claim(JwtRegisteredClaimNames.Sub, user.UserName), + new Claim(JwtRegisteredClaimNames.Sub, user.Id), new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()), }; diff --git a/AdvancedAPI.Business/Services/HouseService.cs b/AdvancedAPI.Business/Services/HouseService.cs deleted file mode 100644 index 6eebafc..0000000 --- a/AdvancedAPI.Business/Services/HouseService.cs +++ /dev/null @@ -1,33 +0,0 @@ -using AdvancedAPI.Data.Models; -using AdvancedAPI.Data.Repositories.Interfaces; -using AdvancedAPI.Data.ViewModels.Houses; -using Business.Services.Interfaces; - -namespace Business.Services; - -/// -public class HouseService : IHouseService -{ - private readonly IHouseRepository _houseRepository; - - /// - /// Constructor. - /// - public HouseService(IHouseRepository houseRepository) - { - _houseRepository = houseRepository; - } - - /// - public async Task?> GetAllHouses(CancellationToken cancellationToken) - { - IEnumerable houses = await _houseRepository.GetAllAsync(); - - if (!houses.Any()) - { - return null; - } - - return new List(); - } -} diff --git a/AdvancedAPI.Business/Services/Interfaces/IHouseService.cs b/AdvancedAPI.Business/Services/Interfaces/IHouseService.cs deleted file mode 100644 index fdc86bd..0000000 --- a/AdvancedAPI.Business/Services/Interfaces/IHouseService.cs +++ /dev/null @@ -1,14 +0,0 @@ -using AdvancedAPI.Data.ViewModels.Houses; - -namespace Business.Services.Interfaces; - -/// -/// Service providing methods for houses. -/// -public interface IHouseService -{ - /// - /// Obtaining List of all houses. - /// - public Task?> GetAllHouses(CancellationToken ct = default); -} diff --git a/AdvancedAPI.Business/Services/Interfaces/IUserService.cs b/AdvancedAPI.Business/Services/Interfaces/IUserService.cs new file mode 100644 index 0000000..7d891a2 --- /dev/null +++ b/AdvancedAPI.Business/Services/Interfaces/IUserService.cs @@ -0,0 +1,19 @@ +using AdvancedAPI.Data.ViewModels.User; + +namespace Business.Services.Interfaces; + +/// +/// User service. +/// +public interface IUserService +{ + /// + /// Gets the user profile of the given user id. + /// + public Task GetUserProfile(string userId); + + /// + /// Updates the last seen state of the user. + /// + public Task UpdateLastSeen(string userId); +} diff --git a/AdvancedAPI.Business/Services/NewsArticleService.cs b/AdvancedAPI.Business/Services/NewsArticleService.cs index 42e0cd5..8d2cf32 100644 --- a/AdvancedAPI.Business/Services/NewsArticleService.cs +++ b/AdvancedAPI.Business/Services/NewsArticleService.cs @@ -26,7 +26,7 @@ public NewsArticleService(ILogger logger, IMapper mapper, IN /// public async Task CreateNewsArticle(NewsArticleRequestModel requestModel) { - var mapped = _mapper.Map(requestModel); + NewsArticle? mapped = _mapper.Map(requestModel); mapped.ReleaseDate = DateTime.Now; try diff --git a/AdvancedAPI.Business/Services/UserService.cs b/AdvancedAPI.Business/Services/UserService.cs new file mode 100644 index 0000000..3048463 --- /dev/null +++ b/AdvancedAPI.Business/Services/UserService.cs @@ -0,0 +1,76 @@ +using System.Globalization; +using AdvancedAPI.Data.Models; +using AdvancedAPI.Data.Repositories.Interfaces; +using AdvancedAPI.Data.ViewModels.User; +using AutoMapper; +using Business.Services.Interfaces; + +namespace Business.Services; + +/// +public class UserService : IUserService +{ + private readonly IIdentityRepository _identityRepository; + private readonly IGenderRepository _genderRepository; + private readonly ILastSeenRepository _lastSeenRepository; + private readonly IMapper _mapper; + + /// + /// Constructor. + /// + public UserService( + IMapper mapper, + IIdentityRepository identityRepository, + IGenderRepository genderRepository, + ILastSeenRepository lastSeenRepository) + { + _identityRepository = identityRepository; + _genderRepository = genderRepository; + _lastSeenRepository = lastSeenRepository; + _mapper = mapper; + } + + /// + public async Task GetUserProfile(string userId) + { + User? user = await _identityRepository.GetUserById(userId); + + if (user == null) + { + return null; + } + + user.Gender = await _genderRepository.GetByIdAsync(user.GenderId); + + UserProfileResponseModel responseModel = _mapper.Map(user); + + responseModel.Birthday = user.DateOfBirth.ToString("MMMM d"); + LastSeen? lastSeen = await _lastSeenRepository.GetByUserId(user.Id); + + responseModel.LastSeen = lastSeen != null ? lastSeen.DateTime.ToString(CultureInfo.InvariantCulture) : "unknown"; + return responseModel; + } + + /// + public async Task UpdateLastSeen(string userId) + { + LastSeen? lastSeen = await _lastSeenRepository.GetByUserId(userId); + + if (lastSeen != null) + { + lastSeen.DateTime = DateTime.Now; + _lastSeenRepository.Update(lastSeen); + } + else + { + await _lastSeenRepository.AddAsync( + new LastSeen() + { + UserId = userId, + DateTime = DateTime.Now, + }); + } + + await _lastSeenRepository.SaveAsync(); + } +} diff --git a/AdvancedAPI.Data/AdvancedApiContext.cs b/AdvancedAPI.Data/AdvancedApiContext.cs index caed2eb..70cc8e3 100644 --- a/AdvancedAPI.Data/AdvancedApiContext.cs +++ b/AdvancedAPI.Data/AdvancedApiContext.cs @@ -8,7 +8,7 @@ namespace AdvancedAPI.Data; /// /// Database context. /// -public class AdvancedApiContext : IdentityDbContext +public class AdvancedApiContext : IdentityDbContext { /// /// Constructor. @@ -22,7 +22,17 @@ public AdvancedApiContext(DbContextOptions options) /// News article database objects. /// public DbSet NewsArticles { get; set; } - + + /// + /// Gender database objects. + /// + public DbSet Genders { get; set; } + + /// + /// LastSeen database objects. + /// + public DbSet LastSeens { get; set; } + /// /// when creating models. /// @@ -30,12 +40,25 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) { base.OnModelCreating(modelBuilder); - // Ensure primary keys are defined for IdentityUserLogin modelBuilder.Entity>(entity => { entity.HasKey(e => new { e.LoginProvider, e.ProviderKey }); }); - // Additional model configurations + modelBuilder.Entity() + .HasOne(u => u.Gender) + .WithMany() + .HasForeignKey(u => u.GenderId); + + modelBuilder.Entity().HasData( + new Gender { Id = 1, Name = "Male" }, + new Gender { Id = 2, Name = "Female" }, + new Gender { Id = 3, Name = "Non-Binary" }, + new Gender { Id = 4, Name = "Other" }); + + modelBuilder.Entity() + .HasOne(u => u.User) + .WithMany() + .HasForeignKey(u => u.UserId); } } diff --git a/AdvancedAPI.Data/DataExtension.cs b/AdvancedAPI.Data/DataExtension.cs new file mode 100644 index 0000000..8de0118 --- /dev/null +++ b/AdvancedAPI.Data/DataExtension.cs @@ -0,0 +1,21 @@ +using AdvancedAPI.Data.Repositories; +using AdvancedAPI.Data.Repositories.Interfaces; + +namespace AdvancedAPI.Data; + +/// +/// Service extension used to prepare the data layer for usage. +/// +public static class DataExtension +{ + /// + /// Registers everything data layer related. + /// + public static void AddDataRepositories(this IServiceCollection services) + { + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + } +} diff --git a/AdvancedAPI.Data/DbInitializer.cs b/AdvancedAPI.Data/DbInitializer.cs index 9c59272..96e1685 100644 --- a/AdvancedAPI.Data/DbInitializer.cs +++ b/AdvancedAPI.Data/DbInitializer.cs @@ -1,4 +1,5 @@ -using Microsoft.AspNetCore.Identity; +using AdvancedAPI.Data.Models; +using Microsoft.AspNetCore.Identity; namespace AdvancedAPI.Data { @@ -12,7 +13,7 @@ public class DbInitializer /// public static async Task Initialize(IServiceProvider serviceProvider) { - UserManager userManager = serviceProvider.GetRequiredService>(); + UserManager userManager = serviceProvider.GetRequiredService>(); RoleManager roleManager = serviceProvider.GetRequiredService>(); // Seed roles @@ -44,15 +45,18 @@ private static async Task SeedRoles(RoleManager roleManager) /// /// Seeding admin user into the database. /// - private static async Task SeedAdminUser(UserManager userManager) + private static async Task SeedAdminUser(UserManager userManager) { - IdentityUser? adminUser = await userManager.FindByEmailAsync("admin@example.com"); + User? adminUser = await userManager.FindByEmailAsync("admin@example.com"); if (adminUser == null) { - adminUser = new IdentityUser + adminUser = new User { UserName = "admin@example.com", Email = "admin@example.com", + DateOfBirth = new DateTime(1980, 1, 1), + DisplayName = "Admin User", + GenderId = 1 // Assuming 1 is the ID for Male }; IdentityResult? result = await userManager.CreateAsync(adminUser, "P@ssw0rd"); @@ -66,21 +70,24 @@ private static async Task SeedAdminUser(UserManager userManager) /// /// Seeding user user into the database. /// - private static async Task SeedUserUser(UserManager userManager) + private static async Task SeedUserUser(UserManager userManager) { - IdentityUser? adminUser = await userManager.FindByEmailAsync("user@example.com"); - if (adminUser == null) + User? userUser = await userManager.FindByEmailAsync("user@example.com"); + if (userUser == null) { - adminUser = new IdentityUser + userUser = new User { UserName = "user@example.com", Email = "user@example.com", + DateOfBirth = new DateTime(1990, 1, 1), + DisplayName = "Regular User", + GenderId = 2 // Assuming 2 is the ID for Female }; - IdentityResult? result = await userManager.CreateAsync(adminUser, "P@ssw0rd"); + IdentityResult? result = await userManager.CreateAsync(userUser, "P@ssw0rd"); if (result.Succeeded) { - await userManager.AddToRoleAsync(adminUser, "User"); + await userManager.AddToRoleAsync(userUser, "User"); } } } diff --git a/AdvancedAPI.Data/Migrations/20240722171019_AddCustomPropertiesToApplicationUser.Designer.cs b/AdvancedAPI.Data/Migrations/20240722171019_AddCustomPropertiesToApplicationUser.Designer.cs new file mode 100644 index 0000000..6acf8b7 --- /dev/null +++ b/AdvancedAPI.Data/Migrations/20240722171019_AddCustomPropertiesToApplicationUser.Designer.cs @@ -0,0 +1,364 @@ +// +using System; +using AdvancedAPI.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace AdvancedAPI.Data.Migrations +{ + [DbContext(typeof(AdvancedApiContext))] + [Migration("20240722171019_AddCustomPropertiesToApplicationUser")] + partial class AddCustomPropertiesToApplicationUser + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "6.0.32") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder, 1L, 1); + + modelBuilder.Entity("AdvancedAPI.Data.Models.Gender", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"), 1L, 1); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("Genders"); + + b.HasData( + new + { + Id = 1, + Name = "Male" + }, + new + { + Id = 2, + Name = "Female" + }, + new + { + Id = 3, + Name = "Non-Binary" + }, + new + { + Id = 4, + Name = "Other" + }); + }); + + modelBuilder.Entity("AdvancedAPI.Data.Models.NewsArticle", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"), 1L, 1); + + b.Property("ContentHtml") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("HeaderText") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ReleaseDate") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.ToTable("NewsArticles"); + }); + + modelBuilder.Entity("AdvancedAPI.Data.Models.User", b => + { + b.Property("Id") + .HasColumnType("nvarchar(450)"); + + b.Property("AccessFailedCount") + .HasColumnType("int"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("nvarchar(max)"); + + b.Property("DateOfBirth") + .HasColumnType("datetime2"); + + b.Property("DisplayName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("EmailConfirmed") + .HasColumnType("bit"); + + b.Property("GenderId") + .HasColumnType("int"); + + b.Property("LockoutEnabled") + .HasColumnType("bit"); + + b.Property("LockoutEnd") + .HasColumnType("datetimeoffset"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("PasswordHash") + .HasColumnType("nvarchar(max)"); + + b.Property("PhoneNumber") + .HasColumnType("nvarchar(max)"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("bit"); + + b.Property("SecurityStamp") + .HasColumnType("nvarchar(max)"); + + b.Property("TwoFactorEnabled") + .HasColumnType("bit"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.HasKey("Id"); + + b.HasIndex("GenderId"); + + b.HasIndex("NormalizedEmail") + .HasDatabaseName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex") + .HasFilter("[NormalizedUserName] IS NOT NULL"); + + b.ToTable("AspNetUsers", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => + { + b.Property("Id") + .HasColumnType("nvarchar(450)"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("NormalizedName") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasDatabaseName("RoleNameIndex") + .HasFilter("[NormalizedName] IS NOT NULL"); + + b.ToTable("AspNetRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"), 1L, 1); + + b.Property("ClaimType") + .HasColumnType("nvarchar(max)"); + + b.Property("ClaimValue") + .HasColumnType("nvarchar(max)"); + + b.Property("RoleId") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"), 1L, 1); + + b.Property("ClaimType") + .HasColumnType("nvarchar(max)"); + + b.Property("ClaimValue") + .HasColumnType("nvarchar(max)"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider") + .HasColumnType("nvarchar(450)"); + + b.Property("ProviderKey") + .HasColumnType("nvarchar(450)"); + + b.Property("ProviderDisplayName") + .HasColumnType("nvarchar(max)"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.Property("UserId") + .HasColumnType("nvarchar(450)"); + + b.Property("RoleId") + .HasColumnType("nvarchar(450)"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("nvarchar(450)"); + + b.Property("LoginProvider") + .HasColumnType("nvarchar(450)"); + + b.Property("Name") + .HasColumnType("nvarchar(450)"); + + b.Property("Value") + .HasColumnType("nvarchar(max)"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens", (string)null); + }); + + modelBuilder.Entity("AdvancedAPI.Data.Models.User", b => + { + b.HasOne("AdvancedAPI.Data.Models.Gender", "Gender") + .WithMany() + .HasForeignKey("GenderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Gender"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("AdvancedAPI.Data.Models.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("AdvancedAPI.Data.Models.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("AdvancedAPI.Data.Models.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("AdvancedAPI.Data.Models.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/AdvancedAPI.Data/Migrations/20240722171019_AddCustomPropertiesToApplicationUser.cs b/AdvancedAPI.Data/Migrations/20240722171019_AddCustomPropertiesToApplicationUser.cs new file mode 100644 index 0000000..b2f1b99 --- /dev/null +++ b/AdvancedAPI.Data/Migrations/20240722171019_AddCustomPropertiesToApplicationUser.cs @@ -0,0 +1,97 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace AdvancedAPI.Data.Migrations +{ + public partial class AddCustomPropertiesToApplicationUser : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "DateOfBirth", + table: "AspNetUsers", + type: "datetime2", + nullable: false, + defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified)); + + migrationBuilder.AddColumn( + name: "DisplayName", + table: "AspNetUsers", + type: "nvarchar(max)", + nullable: false, + defaultValue: ""); + + migrationBuilder.AddColumn( + name: "GenderId", + table: "AspNetUsers", + type: "int", + nullable: false, + defaultValue: 0); + + migrationBuilder.CreateTable( + name: "Genders", + columns: table => new + { + Id = table.Column(type: "int", nullable: false) + .Annotation("SqlServer:Identity", "1, 1"), + Name = table.Column(type: "nvarchar(max)", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Genders", x => x.Id); + }); + + migrationBuilder.InsertData( + table: "Genders", + columns: new[] { "Id", "Name" }, + values: new object[,] + { + { 1, "Male" }, + { 2, "Female" }, + { 3, "Non-Binary" }, + { 4, "Other" } + }); + + migrationBuilder.CreateIndex( + name: "IX_AspNetUsers_GenderId", + table: "AspNetUsers", + column: "GenderId"); + + migrationBuilder.AddForeignKey( + name: "FK_AspNetUsers_Genders_GenderId", + table: "AspNetUsers", + column: "GenderId", + principalTable: "Genders", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_AspNetUsers_Genders_GenderId", + table: "AspNetUsers"); + + migrationBuilder.DropTable( + name: "Genders"); + + migrationBuilder.DropIndex( + name: "IX_AspNetUsers_GenderId", + table: "AspNetUsers"); + + migrationBuilder.DropColumn( + name: "DateOfBirth", + table: "AspNetUsers"); + + migrationBuilder.DropColumn( + name: "DisplayName", + table: "AspNetUsers"); + + migrationBuilder.DropColumn( + name: "GenderId", + table: "AspNetUsers"); + } + } +} diff --git a/AdvancedAPI.Data/Migrations/20240722213835_AddLastSeenObject.Designer.cs b/AdvancedAPI.Data/Migrations/20240722213835_AddLastSeenObject.Designer.cs new file mode 100644 index 0000000..e524c4b --- /dev/null +++ b/AdvancedAPI.Data/Migrations/20240722213835_AddLastSeenObject.Designer.cs @@ -0,0 +1,397 @@ +// +using System; +using AdvancedAPI.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace AdvancedAPI.Data.Migrations +{ + [DbContext(typeof(AdvancedApiContext))] + [Migration("20240722213835_AddLastSeenObject")] + partial class AddLastSeenObject + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "6.0.32") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder, 1L, 1); + + modelBuilder.Entity("AdvancedAPI.Data.Models.Gender", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"), 1L, 1); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("Genders"); + + b.HasData( + new + { + Id = 1, + Name = "Male" + }, + new + { + Id = 2, + Name = "Female" + }, + new + { + Id = 3, + Name = "Non-Binary" + }, + new + { + Id = 4, + Name = "Other" + }); + }); + + modelBuilder.Entity("AdvancedAPI.Data.Models.LastSeen", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"), 1L, 1); + + b.Property("DatetTime") + .HasColumnType("datetime2"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("LastSeens"); + }); + + modelBuilder.Entity("AdvancedAPI.Data.Models.NewsArticle", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"), 1L, 1); + + b.Property("ContentHtml") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("HeaderText") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ReleaseDate") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.ToTable("NewsArticles"); + }); + + modelBuilder.Entity("AdvancedAPI.Data.Models.User", b => + { + b.Property("Id") + .HasColumnType("nvarchar(450)"); + + b.Property("AccessFailedCount") + .HasColumnType("int"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("nvarchar(max)"); + + b.Property("DateOfBirth") + .HasColumnType("datetime2"); + + b.Property("DisplayName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("EmailConfirmed") + .HasColumnType("bit"); + + b.Property("GenderId") + .HasColumnType("int"); + + b.Property("LockoutEnabled") + .HasColumnType("bit"); + + b.Property("LockoutEnd") + .HasColumnType("datetimeoffset"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("PasswordHash") + .HasColumnType("nvarchar(max)"); + + b.Property("PhoneNumber") + .HasColumnType("nvarchar(max)"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("bit"); + + b.Property("SecurityStamp") + .HasColumnType("nvarchar(max)"); + + b.Property("TwoFactorEnabled") + .HasColumnType("bit"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.HasKey("Id"); + + b.HasIndex("GenderId"); + + b.HasIndex("NormalizedEmail") + .HasDatabaseName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex") + .HasFilter("[NormalizedUserName] IS NOT NULL"); + + b.ToTable("AspNetUsers", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => + { + b.Property("Id") + .HasColumnType("nvarchar(450)"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("NormalizedName") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasDatabaseName("RoleNameIndex") + .HasFilter("[NormalizedName] IS NOT NULL"); + + b.ToTable("AspNetRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"), 1L, 1); + + b.Property("ClaimType") + .HasColumnType("nvarchar(max)"); + + b.Property("ClaimValue") + .HasColumnType("nvarchar(max)"); + + b.Property("RoleId") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"), 1L, 1); + + b.Property("ClaimType") + .HasColumnType("nvarchar(max)"); + + b.Property("ClaimValue") + .HasColumnType("nvarchar(max)"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider") + .HasColumnType("nvarchar(450)"); + + b.Property("ProviderKey") + .HasColumnType("nvarchar(450)"); + + b.Property("ProviderDisplayName") + .HasColumnType("nvarchar(max)"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.Property("UserId") + .HasColumnType("nvarchar(450)"); + + b.Property("RoleId") + .HasColumnType("nvarchar(450)"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("nvarchar(450)"); + + b.Property("LoginProvider") + .HasColumnType("nvarchar(450)"); + + b.Property("Name") + .HasColumnType("nvarchar(450)"); + + b.Property("Value") + .HasColumnType("nvarchar(max)"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens", (string)null); + }); + + modelBuilder.Entity("AdvancedAPI.Data.Models.LastSeen", b => + { + b.HasOne("AdvancedAPI.Data.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("AdvancedAPI.Data.Models.User", b => + { + b.HasOne("AdvancedAPI.Data.Models.Gender", "Gender") + .WithMany() + .HasForeignKey("GenderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Gender"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("AdvancedAPI.Data.Models.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("AdvancedAPI.Data.Models.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("AdvancedAPI.Data.Models.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("AdvancedAPI.Data.Models.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/AdvancedAPI.Data/Migrations/20240722213835_AddLastSeenObject.cs b/AdvancedAPI.Data/Migrations/20240722213835_AddLastSeenObject.cs new file mode 100644 index 0000000..0a74ac9 --- /dev/null +++ b/AdvancedAPI.Data/Migrations/20240722213835_AddLastSeenObject.cs @@ -0,0 +1,44 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace AdvancedAPI.Data.Migrations +{ + public partial class AddLastSeenObject : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "LastSeens", + columns: table => new + { + Id = table.Column(type: "int", nullable: false) + .Annotation("SqlServer:Identity", "1, 1"), + UserId = table.Column(type: "nvarchar(450)", nullable: false), + DateTime = table.Column(type: "datetime2", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_LastSeens", x => x.Id); + table.ForeignKey( + name: "FK_LastSeens_AspNetUsers_UserId", + column: x => x.UserId, + principalTable: "AspNetUsers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_LastSeens_UserId", + table: "LastSeens", + column: "UserId"); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "LastSeens"); + } + } +} diff --git a/AdvancedAPI.Data/Migrations/AdvancedApiContextModelSnapshot.cs b/AdvancedAPI.Data/Migrations/AdvancedApiContextModelSnapshot.cs index 5a8b639..8a1e9fc 100644 --- a/AdvancedAPI.Data/Migrations/AdvancedApiContextModelSnapshot.cs +++ b/AdvancedAPI.Data/Migrations/AdvancedApiContextModelSnapshot.cs @@ -22,7 +22,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder, 1L, 1); - modelBuilder.Entity("AdvancedAPI.Data.Models.NewsArticle", b => + modelBuilder.Entity("AdvancedAPI.Data.Models.Gender", b => { b.Property("Id") .ValueGeneratedOnAdd() @@ -30,50 +30,60 @@ protected override void BuildModel(ModelBuilder modelBuilder) SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"), 1L, 1); - b.Property("ContentHtml") - .IsRequired() - .HasColumnType("nvarchar(max)"); - - b.Property("HeaderText") + b.Property("Name") .IsRequired() .HasColumnType("nvarchar(max)"); - b.Property("ReleaseDate") - .HasColumnType("datetime2"); - b.HasKey("Id"); - b.ToTable("NewsArticles"); + b.ToTable("Genders"); + + b.HasData( + new + { + Id = 1, + Name = "Male" + }, + new + { + Id = 2, + Name = "Female" + }, + new + { + Id = 3, + Name = "Non-Binary" + }, + new + { + Id = 4, + Name = "Other" + }); }); - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => + modelBuilder.Entity("AdvancedAPI.Data.Models.LastSeen", b => { - b.Property("Id") - .HasColumnType("nvarchar(450)"); + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); - b.Property("ConcurrencyStamp") - .IsConcurrencyToken() - .HasColumnType("nvarchar(max)"); + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"), 1L, 1); - b.Property("Name") - .HasMaxLength(256) - .HasColumnType("nvarchar(256)"); + b.Property("DateTime") + .HasColumnType("datetime2"); - b.Property("NormalizedName") - .HasMaxLength(256) - .HasColumnType("nvarchar(256)"); + b.Property("UserId") + .IsRequired() + .HasColumnType("nvarchar(450)"); b.HasKey("Id"); - b.HasIndex("NormalizedName") - .IsUnique() - .HasDatabaseName("RoleNameIndex") - .HasFilter("[NormalizedName] IS NOT NULL"); + b.HasIndex("UserId"); - b.ToTable("AspNetRoles", (string)null); + b.ToTable("LastSeens"); }); - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + modelBuilder.Entity("AdvancedAPI.Data.Models.NewsArticle", b => { b.Property("Id") .ValueGeneratedOnAdd() @@ -81,24 +91,23 @@ protected override void BuildModel(ModelBuilder modelBuilder) SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"), 1L, 1); - b.Property("ClaimType") + b.Property("ContentHtml") + .IsRequired() .HasColumnType("nvarchar(max)"); - b.Property("ClaimValue") + b.Property("HeaderText") + .IsRequired() .HasColumnType("nvarchar(max)"); - b.Property("RoleId") - .IsRequired() - .HasColumnType("nvarchar(450)"); + b.Property("ReleaseDate") + .HasColumnType("datetime2"); b.HasKey("Id"); - b.HasIndex("RoleId"); - - b.ToTable("AspNetRoleClaims", (string)null); + b.ToTable("NewsArticles"); }); - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUser", b => + modelBuilder.Entity("AdvancedAPI.Data.Models.User", b => { b.Property("Id") .HasColumnType("nvarchar(450)"); @@ -110,6 +119,13 @@ protected override void BuildModel(ModelBuilder modelBuilder) .IsConcurrencyToken() .HasColumnType("nvarchar(max)"); + b.Property("DateOfBirth") + .HasColumnType("datetime2"); + + b.Property("DisplayName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + b.Property("Email") .HasMaxLength(256) .HasColumnType("nvarchar(256)"); @@ -117,6 +133,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("EmailConfirmed") .HasColumnType("bit"); + b.Property("GenderId") + .HasColumnType("int"); + b.Property("LockoutEnabled") .HasColumnType("bit"); @@ -152,6 +171,8 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasKey("Id"); + b.HasIndex("GenderId"); + b.HasIndex("NormalizedEmail") .HasDatabaseName("EmailIndex"); @@ -163,6 +184,58 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("AspNetUsers", (string)null); }); + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => + { + b.Property("Id") + .HasColumnType("nvarchar(450)"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("NormalizedName") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasDatabaseName("RoleNameIndex") + .HasFilter("[NormalizedName] IS NOT NULL"); + + b.ToTable("AspNetRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"), 1L, 1); + + b.Property("ClaimType") + .HasColumnType("nvarchar(max)"); + + b.Property("ClaimValue") + .HasColumnType("nvarchar(max)"); + + b.Property("RoleId") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims", (string)null); + }); + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => { b.Property("Id") @@ -244,6 +317,28 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("AspNetUserTokens", (string)null); }); + modelBuilder.Entity("AdvancedAPI.Data.Models.LastSeen", b => + { + b.HasOne("AdvancedAPI.Data.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("AdvancedAPI.Data.Models.User", b => + { + b.HasOne("AdvancedAPI.Data.Models.Gender", "Gender") + .WithMany() + .HasForeignKey("GenderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Gender"); + }); + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => { b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) @@ -255,7 +350,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => { - b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null) + b.HasOne("AdvancedAPI.Data.Models.User", null) .WithMany() .HasForeignKey("UserId") .OnDelete(DeleteBehavior.Cascade) @@ -264,7 +359,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => { - b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null) + b.HasOne("AdvancedAPI.Data.Models.User", null) .WithMany() .HasForeignKey("UserId") .OnDelete(DeleteBehavior.Cascade) @@ -279,7 +374,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) .OnDelete(DeleteBehavior.Cascade) .IsRequired(); - b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null) + b.HasOne("AdvancedAPI.Data.Models.User", null) .WithMany() .HasForeignKey("UserId") .OnDelete(DeleteBehavior.Cascade) @@ -288,7 +383,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => { - b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null) + b.HasOne("AdvancedAPI.Data.Models.User", null) .WithMany() .HasForeignKey("UserId") .OnDelete(DeleteBehavior.Cascade) diff --git a/AdvancedAPI.Data/Models/Gender.cs b/AdvancedAPI.Data/Models/Gender.cs new file mode 100644 index 0000000..178e09e --- /dev/null +++ b/AdvancedAPI.Data/Models/Gender.cs @@ -0,0 +1,20 @@ +using System.ComponentModel.DataAnnotations; + +namespace AdvancedAPI.Data.Models; + +/// +/// Gender model. +/// Used as model in case the gender list needs expansion. +/// +public class Gender +{ + /// + /// Identifier of gender. + /// + public int Id { get; set; } + + /// + /// name of the gender. + /// + public string Name { get; set; } +} diff --git a/AdvancedAPI.Data/Models/LastSeen.cs b/AdvancedAPI.Data/Models/LastSeen.cs new file mode 100644 index 0000000..c77f811 --- /dev/null +++ b/AdvancedAPI.Data/Models/LastSeen.cs @@ -0,0 +1,27 @@ +namespace AdvancedAPI.Data.Models; + +/// +/// Last seen state of the user. +/// +public class LastSeen +{ + /// + /// Id of the LastSeen object. + /// + public int Id { get; set; } + + /// + /// identifier of the user. + /// + public string UserId { get; set; } + + /// + /// User object. + /// + public User User; + + /// + /// DateTime when user was last seen. + /// + public DateTime DateTime { get; set; } +} diff --git a/AdvancedAPI.Data/Models/User.cs b/AdvancedAPI.Data/Models/User.cs new file mode 100644 index 0000000..ca3644c --- /dev/null +++ b/AdvancedAPI.Data/Models/User.cs @@ -0,0 +1,29 @@ +using Microsoft.AspNetCore.Identity; + +namespace AdvancedAPI.Data.Models; + +/// +/// Extension on . +/// +public class User : IdentityUser +{ + /// + /// Display name which will be shown to the public. + /// + public string DisplayName { get; set; } + + /// + /// Date of birth used for validation etc. + /// + public DateTime DateOfBirth { get; set; } + + /// + /// Gender id. + /// + public int GenderId { get; set; } + + /// + /// Gender object. + /// + public Gender Gender { get; set; } +} diff --git a/AdvancedAPI.Data/Repositories/BaseRepository.cs b/AdvancedAPI.Data/Repositories/BaseRepository.cs index a32a8e3..b0c0818 100644 --- a/AdvancedAPI.Data/Repositories/BaseRepository.cs +++ b/AdvancedAPI.Data/Repositories/BaseRepository.cs @@ -1,8 +1,4 @@ -// -// Copyright (c) PlaceholderCompany. All rights reserved. -// - -using System.Linq.Expressions; +using System.Linq.Expressions; using AdvancedAPI.Data.Repositories.Interfaces; using Microsoft.EntityFrameworkCore; @@ -13,7 +9,7 @@ namespace AdvancedAPI.Data.Repositories; /// public class BaseRepository : IBaseRepository where T : class { - private readonly AdvancedApiContext _context; + protected readonly AdvancedApiContext _context; private readonly DbSet _dbSet; /// diff --git a/AdvancedAPI.Data/Repositories/GenderRepository.cs b/AdvancedAPI.Data/Repositories/GenderRepository.cs new file mode 100644 index 0000000..540ecc2 --- /dev/null +++ b/AdvancedAPI.Data/Repositories/GenderRepository.cs @@ -0,0 +1,16 @@ +using AdvancedAPI.Data.Models; +using AdvancedAPI.Data.Repositories.Interfaces; + +namespace AdvancedAPI.Data.Repositories; + +/// +public class GenderRepository : BaseRepository, IGenderRepository +{ + /// + /// Constructor. + /// + public GenderRepository(AdvancedApiContext context) + : base(context) + { + } +} diff --git a/AdvancedAPI.Data/Repositories/IdentityRepository.cs b/AdvancedAPI.Data/Repositories/IdentityRepository.cs index db4c594..c33c273 100644 --- a/AdvancedAPI.Data/Repositories/IdentityRepository.cs +++ b/AdvancedAPI.Data/Repositories/IdentityRepository.cs @@ -1,4 +1,5 @@ -using AdvancedAPI.Data.Repositories.Interfaces; +using AdvancedAPI.Data.Models; +using AdvancedAPI.Data.Repositories.Interfaces; using Microsoft.AspNetCore.Identity; namespace AdvancedAPI.Data.Repositories; @@ -6,22 +7,25 @@ namespace AdvancedAPI.Data.Repositories; /// public class IdentityRepository : IIdentityRepository { - private readonly UserManager _userManager; + private readonly UserManager _userManager; /// /// Constructor. /// - public IdentityRepository(UserManager userManager) + public IdentityRepository(UserManager userManager) { _userManager = userManager; } /// - public async Task GetUser(string userName) => await _userManager.FindByNameAsync(userName); + public async Task GetUserByName(string userName) => await _userManager.FindByNameAsync(userName); /// - public async Task CheckPassword(IdentityUser user, string password) => + public async Task GetUserById(string userId) => await _userManager.FindByIdAsync(userId); + + /// + public async Task CheckPassword(User user, string password) => await _userManager.CheckPasswordAsync(user, password); - public async Task> GetRoles(IdentityUser user) => await _userManager.GetRolesAsync(user); + public async Task> GetRoles(User user) => await _userManager.GetRolesAsync(user); } diff --git a/AdvancedAPI.Data/Repositories/Interfaces/IGenderRepository.cs b/AdvancedAPI.Data/Repositories/Interfaces/IGenderRepository.cs new file mode 100644 index 0000000..36b2b40 --- /dev/null +++ b/AdvancedAPI.Data/Repositories/Interfaces/IGenderRepository.cs @@ -0,0 +1,10 @@ +using AdvancedAPI.Data.Models; + +namespace AdvancedAPI.Data.Repositories.Interfaces; + +/// +/// Repository for genders. +/// +public interface IGenderRepository : IBaseRepository +{ +} diff --git a/AdvancedAPI.Data/Repositories/Interfaces/IIdentityRepository.cs b/AdvancedAPI.Data/Repositories/Interfaces/IIdentityRepository.cs index a45e1e7..d04903c 100644 --- a/AdvancedAPI.Data/Repositories/Interfaces/IIdentityRepository.cs +++ b/AdvancedAPI.Data/Repositories/Interfaces/IIdentityRepository.cs @@ -1,4 +1,5 @@ -using Microsoft.AspNetCore.Identity; +using AdvancedAPI.Data.Models; +using Microsoft.AspNetCore.Identity; namespace AdvancedAPI.Data.Repositories.Interfaces; @@ -8,17 +9,22 @@ namespace AdvancedAPI.Data.Repositories.Interfaces; public interface IIdentityRepository { /// - /// Getting the user from User manager. + /// Getting the user from User manager by name. /// - public Task GetUser(string userName); + public Task GetUserByName(string userName); + + /// + /// Getting user from user manager by id. + /// + public Task GetUserById(string userId); /// /// Checks the password of the user with user manager. /// - public Task CheckPassword(IdentityUser user, string password); + public Task CheckPassword(User user, string password); /// /// Getting a list of roles assigned to the user. /// - public Task> GetRoles(IdentityUser user); + public Task> GetRoles(User user); } diff --git a/AdvancedAPI.Data/Repositories/Interfaces/ILastSeenRepository.cs b/AdvancedAPI.Data/Repositories/Interfaces/ILastSeenRepository.cs new file mode 100644 index 0000000..2df06a6 --- /dev/null +++ b/AdvancedAPI.Data/Repositories/Interfaces/ILastSeenRepository.cs @@ -0,0 +1,14 @@ +using AdvancedAPI.Data.Models; + +namespace AdvancedAPI.Data.Repositories.Interfaces; + +/// +/// LastSeen Repository. +/// +public interface ILastSeenRepository : IBaseRepository +{ + /// + /// Getting single object of Last seen by user id. + /// + public Task GetByUserId(string userId); +} diff --git a/AdvancedAPI.Data/Repositories/LastSeenRepository.cs b/AdvancedAPI.Data/Repositories/LastSeenRepository.cs new file mode 100644 index 0000000..1f46e6b --- /dev/null +++ b/AdvancedAPI.Data/Repositories/LastSeenRepository.cs @@ -0,0 +1,20 @@ +using AdvancedAPI.Data.Models; +using AdvancedAPI.Data.Repositories.Interfaces; +using Microsoft.EntityFrameworkCore; + +namespace AdvancedAPI.Data.Repositories; + +/// +public class LastSeenRepository : BaseRepository, ILastSeenRepository +{ + /// + /// Constructor. + /// + public LastSeenRepository(AdvancedApiContext context) + : base(context) + { + } + + /// + public async Task GetByUserId(string userId) => await _context.LastSeens.FirstOrDefaultAsync(ls => ls.UserId == userId); +} diff --git a/AdvancedAPI.Data/ViewModels/Houses/HouseResponseModel.cs b/AdvancedAPI.Data/ViewModels/Houses/HouseResponseModel.cs deleted file mode 100644 index 0d3db78..0000000 --- a/AdvancedAPI.Data/ViewModels/Houses/HouseResponseModel.cs +++ /dev/null @@ -1,16 +0,0 @@ -using Newtonsoft.Json; - -namespace AdvancedAPI.Data.ViewModels.Houses; - -/// -/// House response model. -/// -[JsonObject] -public class HouseResponseModel -{ - /// - /// Name of the street where the house is located. - /// - [JsonProperty("street name")] - public string StreetName { get; set; } = string.Empty; -} diff --git a/AdvancedAPI.Data/ViewModels/User/UserProfileResponseModel.cs b/AdvancedAPI.Data/ViewModels/User/UserProfileResponseModel.cs new file mode 100644 index 0000000..c5971e1 --- /dev/null +++ b/AdvancedAPI.Data/ViewModels/User/UserProfileResponseModel.cs @@ -0,0 +1,32 @@ +namespace AdvancedAPI.Data.ViewModels.User; + +/// +/// User profike response model. +/// +public class UserProfileResponseModel +{ + /// + /// identifier of the user. + /// + public string Id { get; set; } + + /// + /// display name of the user. + /// + public string DisplayName { get; set; } + + /// + /// Birthday of the user. + /// + public string Birthday { get; set; } + + /// + /// Gender of the user. + /// + public string Gender { get; set; } + + /// + /// Timespan of how long ago the user + /// + public string LastSeen { get; set; } +} diff --git a/AdvancedAPI.Data/appsettings.json b/AdvancedAPI.Data/appsettings.json index 3a6e1a6..e667dfb 100644 --- a/AdvancedAPI.Data/appsettings.json +++ b/AdvancedAPI.Data/appsettings.json @@ -1,6 +1,6 @@ { "ConnectionStrings": { - "DefaultConnection": "Server=DESKTOP-HFMAPSN\\SQLEXPRESS;Database=master;Trusted_Connection=True;Integrated Security=True;" + "DefaultConnection": "Server=DESKTOP-HFMAPSN\\SQLEXPRESS;Database=AdvancedApi;Trusted_Connection=True;Integrated Security=True;" }, "Logging": { "LogLevel": { diff --git a/AdvancedAPI.Tests/Services/HouseServiceTests.cs b/AdvancedAPI.Tests/Services/HouseServiceTests.cs deleted file mode 100644 index 377f193..0000000 --- a/AdvancedAPI.Tests/Services/HouseServiceTests.cs +++ /dev/null @@ -1,51 +0,0 @@ -using AdvancedAPI.Data.Models; -using AdvancedAPI.Data.Repositories.Interfaces; -using AdvancedAPI.Data.ViewModels.Houses; -using AutoFixture; -using Business.Services; -using Business.Services.Interfaces; -using Moq; - -namespace Tests.Services; - -/// -/// all tests for . -/// -public class HouseServiceTests -{ - private readonly IHouseService _houseService; - private readonly Mock _houseRepository; - - /// - /// Constructor. - /// - public HouseServiceTests() - { - var fixture = new Fixture(); - _houseRepository = fixture.Freeze>(); - _houseService = new HouseService(_houseRepository.Object); - } - - /// - /// Tests if returns a valid list. - /// - [Fact] - public async Task GetAllHousesReturnsNull() - { - _houseRepository.Setup(x => x.GetAllAsync()).ReturnsAsync(new List()); - List? result = await _houseService.GetAllHouses(); - - Assert.Null(result); - } - - /// - /// Tests if returns a valid list. - /// - [Fact] - public async Task GetAllHousesReturnsList() - { - _houseRepository.Setup(x => x.GetAllAsync()).ReturnsAsync(new List { new() }); - List? result = await _houseService.GetAllHouses(); - Assert.Equal(new List(), result); - } -} diff --git a/AdvancedAPI.Tests/Services/NewsArticleTests.cs b/AdvancedAPI.Tests/Services/NewsArticleTests.cs index 8c28474..75597cc 100644 --- a/AdvancedAPI.Tests/Services/NewsArticleTests.cs +++ b/AdvancedAPI.Tests/Services/NewsArticleTests.cs @@ -62,7 +62,7 @@ public async Task CreateNewsArticleCreateFailedOnAdd() _newsArticleRepository.Setup(r => r.AddAsync(It.IsAny())).Returns(Task.CompletedTask); _newsArticleRepository.Setup(r => r.SaveAsync()).Throws(It.IsAny()); - var exception = await Assert.ThrowsAsync(() => _newsArticleService.CreateNewsArticle(requestModel)); + Exception? exception = await Assert.ThrowsAsync(() => _newsArticleService.CreateNewsArticle(requestModel)); Assert.Equal("Could not insert news article", exception.Message); } diff --git a/AdvancedAPI.Tests/Services/UserServiceTests.cs b/AdvancedAPI.Tests/Services/UserServiceTests.cs new file mode 100644 index 0000000..d93437e --- /dev/null +++ b/AdvancedAPI.Tests/Services/UserServiceTests.cs @@ -0,0 +1,186 @@ +using System.Globalization; +using AdvancedAPI.Data.Models; +using AdvancedAPI.Data.Repositories.Interfaces; +using AdvancedAPI.Data.ViewModels.User; +using AutoFixture; +using AutoFixture.AutoMoq; +using AutoMapper; +using Business.Services; +using Business.Services.Interfaces; +using Moq; + +namespace Tests.Services; + +/// +/// Tests of the user service. +/// +public class UserServiceTests +{ + private readonly IFixture _fixture; + private readonly IUserService _UserService; + private readonly Mock _identityRepository; + private readonly Mock _genderRepository; + private readonly Mock _lastSeenRepository; + private readonly Mock _mapper; + + /// + /// Constructor. + /// + public UserServiceTests() + { + _fixture = new Fixture().Customize(new AutoMoqCustomization()); + _identityRepository = _fixture.Freeze>(); + _genderRepository = _fixture.Freeze>(); + _lastSeenRepository = _fixture.Freeze>(); + _mapper = _fixture.Freeze>(); + _UserService = new UserService( + _mapper.Object, + _identityRepository.Object, + _genderRepository.Object, + _lastSeenRepository.Object); + } + + /// + /// Tests if GetUserProfile returns null when there is an invalid userid given. + /// + [Fact] + public async Task GetUserProfileReturnsNullOnWrongUserId() + { + string userId = "invalid-user-id"; + _identityRepository.Setup(ir => ir.GetUserById(It.IsAny())).ReturnsAsync((User?)null); + + UserProfileResponseModel? response = await _UserService.GetUserProfile(userId); + + Assert.Null(response); + } + + /// + /// Tests if GetUserProfile returns a profile with last seen set to unknown. + /// + [Fact] + public async Task GetUserProfileReturnsSuccessfullyWithUnknownLastSeen() + { + string userId = "valid-user-id"; + User userResult = new User + { + UserName = "unittest@test.dev", + Email = "unittest@test.dev", + DisplayName = "Unit test user", + DateOfBirth = new DateTime(2024, 7, 23), + Gender = new Gender + { + Id = 1, + Name = "Robot", + }, + }; + Gender genderResult = new Gender + { + Id = 1, + Name = "Robot", + }; + + _identityRepository.Setup(ir => ir.GetUserById(It.IsAny())).ReturnsAsync(userResult); + _genderRepository.Setup(gr => gr.GetByIdAsync(It.IsAny())).ReturnsAsync(genderResult); + _lastSeenRepository.Setup(lsr => lsr.GetByUserId(It.IsAny())).ReturnsAsync((LastSeen?)null); + UserProfileResponseModel? response = await _UserService.GetUserProfile(userId); + + Assert.Equal("unknown", response.LastSeen); + } + + /// + /// Tests if GetUserProfile returns a profile with a filled last seen. + /// + [Fact] + public async Task GetUserProfileReturnsSuccessfullyWithFilledLastSeen() + { + string userId = "valid-user-id"; + User userResult = new User + { + UserName = "unittest@test.dev", + Email = "unittest@test.dev", + DisplayName = "Unit test user", + DateOfBirth = new DateTime(2024, 7, 23), + Gender = new Gender + { + Id = 1, + Name = "Robot", + }, + }; + + LastSeen lastSeenResult = new LastSeen + { + Id = 1, + UserId = userId, + DateTime = DateTime.Now, + }; + + Gender genderResult = new Gender + { + Id = 1, + Name = "Robot", + }; + + _identityRepository.Setup(ir => ir.GetUserById(It.IsAny())).ReturnsAsync(userResult); + _genderRepository.Setup(gr => gr.GetByIdAsync(It.IsAny())).ReturnsAsync(genderResult); + _lastSeenRepository.Setup(lsr => lsr.GetByUserId(It.IsAny())).ReturnsAsync(lastSeenResult); + UserProfileResponseModel? response = await _UserService.GetUserProfile(userId); + + Assert.Equal(lastSeenResult.DateTime.ToString(CultureInfo.InvariantCulture), response.LastSeen); + } + + /// + /// Tests if UpdateLastSeen calls the AddAsync of the LastSeen Repo just once. + /// + [Fact] + public async Task UpdateLastSeenCreatesNew() + { + string userId = "valid-user-id"; + _lastSeenRepository.Setup(lsr => lsr.GetByUserId(It.IsAny())).ReturnsAsync((LastSeen?)null); + + await _UserService.UpdateLastSeen(userId); + + _lastSeenRepository.Verify(lsr => lsr.AddAsync(It.IsAny()), Times.Once); + } + + /// + /// Tests if UpdateLastSeen calls the Update of the LastSeen Repo just once. + /// + [Fact] + public async Task UpdateLastSeenUpdates() + { + string userId = "valid-user-id"; + LastSeen lastSeenResult = new LastSeen + { + Id = 1, + UserId = userId, + DateTime = DateTime.Now, + }; + + _lastSeenRepository.Setup(lsr => lsr.GetByUserId(It.IsAny())).ReturnsAsync(lastSeenResult); + + await _UserService.UpdateLastSeen(userId); + + _lastSeenRepository.Verify(lsr => lsr.Update(It.IsAny()), Times.Once); + } + + /// + /// Tests if UpdateLastSeen calls the Saves the database changes. + /// + [Fact] + public async Task UpdateLastSeenSaves() + { + string userId = "valid-user-id"; + LastSeen lastSeenResult = new LastSeen + { + Id = 1, + UserId = userId, + DateTime = DateTime.Now, + }; + + _lastSeenRepository.Setup(lsr => lsr.GetByUserId(It.IsAny())).ReturnsAsync(lastSeenResult); + + await _UserService.UpdateLastSeen(userId); + + _lastSeenRepository.Verify(lsr => lsr.SaveAsync(), Times.Once); + } +} diff --git a/AdvancedAPI/ActionFilters/LogLastSeenActionFilter.cs b/AdvancedAPI/ActionFilters/LogLastSeenActionFilter.cs new file mode 100644 index 0000000..08fe90d --- /dev/null +++ b/AdvancedAPI/ActionFilters/LogLastSeenActionFilter.cs @@ -0,0 +1,40 @@ +using System.Security.Claims; +using Business.Services.Interfaces; +using Microsoft.AspNetCore.Mvc.Filters; + +namespace AdvancedAPI.ActionFilters; + +/// +/// Last seen action filter. +/// +public class LogLastSeenActionFilter : IAsyncActionFilter +{ + private readonly IUserService _userService; + + /// + /// Constructor + /// + public LogLastSeenActionFilter(IUserService userService) + { + _userService = userService; + } + + /// + public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next) + { + ClaimsPrincipal user = context.HttpContext.User; + + if (user.Identity.IsAuthenticated) + { + string? userId = user.FindFirst(ClaimTypes.NameIdentifier)?.Value; + + + if (!string.IsNullOrEmpty(userId)) + { + await _userService.UpdateLastSeen(userId); + } + } + + await next(); + } +} diff --git a/AdvancedAPI/BaseControllers/AdminBaseController.cs b/AdvancedAPI/BaseControllers/AdminBaseController.cs index 65c3396..2c01538 100644 --- a/AdvancedAPI/BaseControllers/AdminBaseController.cs +++ b/AdvancedAPI/BaseControllers/AdminBaseController.cs @@ -1,6 +1,5 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Mvc.Filters; namespace AdvancedAPI.BaseControllers; diff --git a/AdvancedAPI/BaseControllers/UserBaseController.cs b/AdvancedAPI/BaseControllers/UserBaseController.cs new file mode 100644 index 0000000..32f83ab --- /dev/null +++ b/AdvancedAPI/BaseControllers/UserBaseController.cs @@ -0,0 +1,17 @@ +using Microsoft.AspNetCore.Authorization; + +namespace AdvancedAPI.BaseControllers; + +/// +/// Controller for admin related endpoints. +/// +[Authorize(Policy = "UserOrAdmin")] +public class UserBaseController : BaseController +{ + /// + /// Constructor. + /// + public UserBaseController() + { + } +} \ No newline at end of file diff --git a/AdvancedAPI/Controllers/Admin/DashboardController.cs b/AdvancedAPI/Controllers/Admin/DashboardController.cs index 01b0b0b..d2eb9bc 100644 --- a/AdvancedAPI/Controllers/Admin/DashboardController.cs +++ b/AdvancedAPI/Controllers/Admin/DashboardController.cs @@ -1,8 +1,4 @@ -// -// Copyright (c) PlaceholderCompany. All rights reserved. -// - -using AdvancedAPI.BaseControllers; +using AdvancedAPI.BaseControllers; using Microsoft.AspNetCore.Mvc; namespace AdvancedAPI.Controllers.Admin; diff --git a/AdvancedAPI/Controllers/AuthenticationContoller.cs b/AdvancedAPI/Controllers/AuthenticationContoller.cs index b5a5f69..336551b 100644 --- a/AdvancedAPI/Controllers/AuthenticationContoller.cs +++ b/AdvancedAPI/Controllers/AuthenticationContoller.cs @@ -13,15 +13,13 @@ namespace AdvancedAPI.Controllers; public class AuthenticationContoller : BaseController { private readonly IAuthenticationService _authenticationService; - private readonly IConfiguration _configuration; /// /// Constructor. /// - public AuthenticationContoller(IAuthenticationService authenticationService, IConfiguration configuration) + public AuthenticationContoller(IAuthenticationService authenticationService) { _authenticationService = authenticationService; - _configuration = configuration; } /// diff --git a/AdvancedAPI/Controllers/HouseController.cs b/AdvancedAPI/Controllers/HouseController.cs deleted file mode 100644 index 356c4c5..0000000 --- a/AdvancedAPI/Controllers/HouseController.cs +++ /dev/null @@ -1,43 +0,0 @@ -using AdvancedAPI.BaseControllers; -using AdvancedAPI.Data.ViewModels; -using AdvancedAPI.Data.ViewModels.Houses; -using Business.Services.Interfaces; -using Microsoft.AspNetCore.Mvc; - -namespace AdvancedAPI.Controllers; - -/// -/// House api provided operation to main the houses. -/// -[Route("house")] -public class HouseController : BaseController -{ - private readonly IHouseService _houseService; - - private HouseController(IHouseService houseService) - { - _houseService = houseService; - } - - /// - /// Obtains a list of houses. - /// - /// List of houses. - [HttpGet] - [Route("overview")] - [ProducesResponseType(typeof(List), 200)] - [ProducesResponseType(typeof(ErrorResponseModel), 401)] - [ProducesResponseType(typeof(ErrorResponseModel), 404)] - [ProducesResponseType(typeof(ErrorResponseModel), 500)] - public async Task Overview(CancellationToken cancellationToken = default) - { - List? houseResponseModels = await _houseService.GetAllHouses(cancellationToken); - - if (houseResponseModels == null || !houseResponseModels.Any()) - { - return NotFoundResult("Could not find any houses"); - } - - return Ok(new List()); - } -} diff --git a/AdvancedAPI/Controllers/User/UserController.cs b/AdvancedAPI/Controllers/User/UserController.cs new file mode 100644 index 0000000..9f9aec4 --- /dev/null +++ b/AdvancedAPI/Controllers/User/UserController.cs @@ -0,0 +1,45 @@ +using AdvancedAPI.BaseControllers; +using AdvancedAPI.Data.ViewModels.User; +using Business.Services.Interfaces; +using Microsoft.AspNetCore.Mvc; + +namespace AdvancedAPI.Controllers.User; + +/// +/// User endpoints +/// +[Route("user")] +public class UserController : UserBaseController +{ + private readonly IUserService _userService; + + /// + /// Constructor. + /// + public UserController(IUserService userService) + { + _userService = userService; + } + + /// + /// Get the user profile of the given user id. + /// + [HttpGet] + [Route("profile")] + public async Task GetUserProfile(string userId) + { + if (string.IsNullOrEmpty(userId)) + { + return BadRequestResult("User id can't be empty"); + } + + UserProfileResponseModel? userProfile = await _userService.GetUserProfile(userId); + + if (userProfile == null) + { + return NotFound("There is no user with this userId"); + } + + return Ok(userProfile); + } +} diff --git a/AdvancedAPI/Program.cs b/AdvancedAPI/Program.cs index 8df40cf..072df6d 100644 --- a/AdvancedAPI/Program.cs +++ b/AdvancedAPI/Program.cs @@ -1,8 +1,8 @@ using System.Reflection; using System.Text; +using AdvancedAPI.ActionFilters; using AdvancedAPI.Data; -using AdvancedAPI.Data.Repositories; -using AdvancedAPI.Data.Repositories.Interfaces; +using AdvancedAPI.Data.Models; using Business; using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.AspNetCore.Identity; @@ -10,17 +10,20 @@ using Microsoft.IdentityModel.Tokens; using Microsoft.OpenApi.Models; -var builder = WebApplication.CreateBuilder(args); +WebApplicationBuilder builder = WebApplication.CreateBuilder(args); // Add services to the container. -builder.Services.AddControllersWithViews(); +builder.Services.AddControllers( + options => + { + options.Filters.Add(); + }); // Services. builder.Services.AddBusinessServices(); // Repositories. -builder.Services.AddScoped(); -builder.Services.AddScoped(); +builder.Services.AddDataRepositories(); builder.Services.AddSwaggerGen( c => @@ -34,8 +37,8 @@ Description = "The Advanced API of DustSwiffer", }); - var xmlFile = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml"; - var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile); + string xmlFile = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml"; + string xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile); c.IncludeXmlComments(xmlPath); c.AddSecurityDefinition( @@ -71,13 +74,13 @@ }); }); -var connectionString = builder.Configuration.GetConnectionString("DefaultConnection"); +string connectionString = builder.Configuration.GetConnectionString("DefaultConnection"); builder.Services.AddDbContext( options => options.UseSqlServer(connectionString)); -builder.Services.AddIdentity() +builder.Services.AddIdentity() .AddEntityFrameworkStores() .AddDefaultTokenProviders(); @@ -113,9 +116,9 @@ policy => policy.RequireRole("Admin")); options.AddPolicy( - "UserPolicy", + "UserOrAdmin", policy => - policy.RequireRole("User")); + policy.RequireRole("User", "Admin")); }); WebApplication app = builder.Build(); diff --git a/AdvancedAPI/appsettings.json b/AdvancedAPI/appsettings.json index 3d9ca42..e305c82 100644 --- a/AdvancedAPI/appsettings.json +++ b/AdvancedAPI/appsettings.json @@ -1,6 +1,6 @@ { "ConnectionStrings": { - "DefaultConnection": "Server=DESKTOP-HFMAPSN\\SQLEXPRESS;Database=master;Trusted_Connection=True;Integrated Security=True;" + "DefaultConnection": "Server=DESKTOP-HFMAPSN\\SQLEXPRESS;Database=AdvancedApi;Trusted_Connection=True;Integrated Security=True;" }, "Jwt": { "Key": "YourVeryLongAndSecureKeyForJWTAuthenticationWhichShouldBeAtLeast32Bytes",