From e67906b0dcc76f53160af66741ac7d735b6eab06 Mon Sep 17 00:00:00 2001 From: Alper Soy <51034169+Alper-Soy@users.noreply.github.com> Date: Tue, 20 Aug 2024 12:11:43 +0300 Subject: [PATCH 1/2] AH-12 server side identity (#17) --- API/API.csproj | 8 +- API/Contracts/Auth/AuthUserDto.cs | 9 + API/Contracts/Auth/LoginDto.cs | 7 + API/Contracts/Auth/RegisterDto.cs | 16 + API/Controllers/AccountController.cs | 75 +++++ API/Controllers/BuggyController.cs | 44 ++- API/Extensions/IdentityServiceExtensions.cs | 38 +++ API/Middleware/ExceptionMiddleware.cs | 2 +- API/Program.cs | 15 +- API/Services/Auth/TokenService.cs | 36 +++ API/activity-hub.db | Bin 20480 -> 20480 bytes API/activity-hub.db-shm | Bin 32768 -> 32768 bytes API/activity-hub.db-wal | Bin 12392 -> 576832 bytes API/appsettings.Development.json | 3 +- Application/Core/Result.cs | 4 +- Domain/Domain.csproj | 4 + Domain/Entities/User.cs | 9 + Persistence/DataContext.cs | 3 +- .../20240805112415_InitialCreate.Designer.cs | 54 ---- .../20240805112415_InitialCreate.cs | 39 --- .../20240820065605_InitialCreate.Designer.cs | 303 ++++++++++++++++++ .../20240820065605_InitialCreate.cs | 244 ++++++++++++++ .../Migrations/DataContextModelSnapshot.cs | 253 ++++++++++++++- Persistence/Persistence.csproj | 6 +- Persistence/Seed.cs | 15 +- 25 files changed, 1059 insertions(+), 128 deletions(-) create mode 100644 API/Contracts/Auth/AuthUserDto.cs create mode 100644 API/Contracts/Auth/LoginDto.cs create mode 100644 API/Contracts/Auth/RegisterDto.cs create mode 100644 API/Controllers/AccountController.cs create mode 100644 API/Extensions/IdentityServiceExtensions.cs create mode 100644 API/Services/Auth/TokenService.cs create mode 100644 Domain/Entities/User.cs delete mode 100644 Persistence/Migrations/20240805112415_InitialCreate.Designer.cs delete mode 100644 Persistence/Migrations/20240805112415_InitialCreate.cs create mode 100644 Persistence/Migrations/20240820065605_InitialCreate.Designer.cs create mode 100644 Persistence/Migrations/20240820065605_InitialCreate.cs diff --git a/API/API.csproj b/API/API.csproj index a28a835..a541df4 100644 --- a/API/API.csproj +++ b/API/API.csproj @@ -11,10 +11,16 @@ - + + all runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + diff --git a/API/Contracts/Auth/AuthUserDto.cs b/API/Contracts/Auth/AuthUserDto.cs new file mode 100644 index 0000000..1edcc2e --- /dev/null +++ b/API/Contracts/Auth/AuthUserDto.cs @@ -0,0 +1,9 @@ +namespace API.Contracts.User; + +public class AuthUserDto +{ + public string DisplayName { get; set; } + public string Token { get; set; } + public string Image { get; set; } + public string Username { get; set; } +} diff --git a/API/Contracts/Auth/LoginDto.cs b/API/Contracts/Auth/LoginDto.cs new file mode 100644 index 0000000..d96c3fc --- /dev/null +++ b/API/Contracts/Auth/LoginDto.cs @@ -0,0 +1,7 @@ +namespace API.Contracts.Auth; + +public class LoginDto +{ + public string Email { get; set; } + public string Password { get; set; } +} diff --git a/API/Contracts/Auth/RegisterDto.cs b/API/Contracts/Auth/RegisterDto.cs new file mode 100644 index 0000000..a3cd662 --- /dev/null +++ b/API/Contracts/Auth/RegisterDto.cs @@ -0,0 +1,16 @@ +using System.ComponentModel.DataAnnotations; + +namespace API.Contracts.Auth; + +public class RegisterDto +{ + [Required] [EmailAddress] public string Email { get; set; } + + [Required] + [RegularExpression("(?=.*\\d)(?=.*[a-z])(?=.*[A-Z]).{4,8}$", ErrorMessage = "Password must be complex")] + public string Password { get; set; } + + [Required] public string DisplayName { get; set; } + + [Required] public string Username { get; set; } +} diff --git a/API/Controllers/AccountController.cs b/API/Controllers/AccountController.cs new file mode 100644 index 0000000..2c42ea0 --- /dev/null +++ b/API/Controllers/AccountController.cs @@ -0,0 +1,75 @@ +using System.Security.Claims; +using API.Contracts.Auth; +using API.Contracts.User; +using API.Services.Auth; +using Domain.Entities; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; + +namespace API.Controllers; + +[ApiController] +[Route("api/[controller]")] +public class AccountController(UserManager userManager, TokenService tokenService) : ControllerBase +{ + [AllowAnonymous] + [HttpPost("login")] + public async Task> Login(LoginDto loginDto) + { + var user = await userManager.FindByEmailAsync(loginDto.Email); + + if (user == null) return Unauthorized(); + + var result = await userManager.CheckPasswordAsync(user, loginDto.Password); + + if (result) return CreateUserObject(user); + + return Unauthorized(); + } + + [AllowAnonymous] + [HttpPost("register")] + public async Task> Register(RegisterDto registerDto) + { + if (await userManager.Users.AnyAsync(x => x.UserName == registerDto.Username)) + return BadRequest("Username is already taken"); + + if (await userManager.Users.AnyAsync(x => x.Email == registerDto.Email)) + return BadRequest("Email is already taken"); + + var user = new User + { + DisplayName = registerDto.DisplayName, + Email = registerDto.Email, + UserName = registerDto.Username + }; + + var result = await userManager.CreateAsync(user, registerDto.Password); + + if (result.Succeeded) return CreateUserObject(user); + + return BadRequest(result.Errors); + } + + [Authorize] + [HttpGet] + public async Task> GetCurrentUser() + { + var user = await userManager.FindByEmailAsync(User.FindFirstValue(ClaimTypes.Email)); + + return CreateUserObject(user); + } + + private AuthUserDto CreateUserObject(User user) + { + return new AuthUserDto + { + DisplayName = user.DisplayName, + Image = null, + Token = tokenService.CreateToken(user), + Username = user.UserName + }; + } +} diff --git a/API/Controllers/BuggyController.cs b/API/Controllers/BuggyController.cs index 2195098..2f07fc3 100644 --- a/API/Controllers/BuggyController.cs +++ b/API/Controllers/BuggyController.cs @@ -1,32 +1,30 @@ -using System; using Microsoft.AspNetCore.Mvc; -namespace API.Controllers +namespace API.Controllers; + +public class BuggyController : BaseApiController { - public class BuggyController : BaseApiController + [HttpGet("not-found")] + public ActionResult GetNotFound() { - [HttpGet("not-found")] - public ActionResult GetNotFound() - { - return NotFound(); - } + return NotFound(); + } - [HttpGet("bad-request")] - public ActionResult GetBadRequest() - { - return BadRequest("This is a bad request"); - } + [HttpGet("bad-request")] + public ActionResult GetBadRequest() + { + return BadRequest("This is a bad request"); + } - [HttpGet("server-error")] - public ActionResult GetServerError() - { - throw new Exception("This is a server error"); - } + [HttpGet("server-error")] + public ActionResult GetServerError() + { + throw new Exception("This is a server error"); + } - [HttpGet("unauthorised")] - public ActionResult GetUnauthorised() - { - return Unauthorized(); - } + [HttpGet("unauthorised")] + public ActionResult GetUnauthorised() + { + return Unauthorized(); } } diff --git a/API/Extensions/IdentityServiceExtensions.cs b/API/Extensions/IdentityServiceExtensions.cs new file mode 100644 index 0000000..71765f2 --- /dev/null +++ b/API/Extensions/IdentityServiceExtensions.cs @@ -0,0 +1,38 @@ +using System.Text; +using API.Services.Auth; +using Domain.Entities; +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.IdentityModel.Tokens; +using Persistence; + +namespace API.Extensions; + +public static class IdentityServiceExtensions +{ + public static IServiceCollection AddIdentityServices(this IServiceCollection services, IConfiguration config) + { + services.AddIdentityCore(opt => + { + opt.Password.RequireNonAlphanumeric = false; + opt.User.RequireUniqueEmail = true; + }) + .AddEntityFrameworkStores(); + + + var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(config["TokenKey"])); + services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme).AddJwtBearer(options => + { + options.TokenValidationParameters = new TokenValidationParameters + { + ValidateIssuerSigningKey = true, + IssuerSigningKey = key, + ValidateIssuer = false, + ValidateAudience = false + }; + }); + + services.AddScoped(); + + return services; + } +} diff --git a/API/Middleware/ExceptionMiddleware.cs b/API/Middleware/ExceptionMiddleware.cs index ad633df..8cacebd 100644 --- a/API/Middleware/ExceptionMiddleware.cs +++ b/API/Middleware/ExceptionMiddleware.cs @@ -29,7 +29,7 @@ public async Task InvokeAsync(HttpContext context) var response = env.IsDevelopment() ? new AppException(context.Response.StatusCode, ex.Message, ex.StackTrace) : new AppException(context.Response.StatusCode, "Internal Server Error"); - + var json = JsonSerializer.Serialize(response, JsonOptions); diff --git a/API/Program.cs b/API/Program.cs index 83e5ea2..0a2266b 100644 --- a/API/Program.cs +++ b/API/Program.cs @@ -1,5 +1,9 @@ using API.Extensions; using API.Middleware; +using Domain.Entities; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Mvc.Authorization; using Microsoft.EntityFrameworkCore; using Persistence; @@ -7,8 +11,13 @@ // Add services to the container. -builder.Services.AddControllers(); +builder.Services.AddControllers(opt => +{ + var policy = new AuthorizationPolicyBuilder().RequireAuthenticatedUser().Build(); + opt.Filters.Add(new AuthorizeFilter(policy)); +}); builder.Services.AddApplicationServices(builder.Configuration); +builder.Services.AddIdentityServices(builder.Configuration); var app = builder.Build(); @@ -17,6 +26,7 @@ app.UseCors("CorsPolicy"); +app.UseAuthentication(); app.UseAuthorization(); app.MapControllers(); @@ -27,8 +37,9 @@ try { var context = services.GetRequiredService(); + var userManager = services.GetRequiredService>(); await context.Database.MigrateAsync(); - await Seed.SeedData(context); + await Seed.SeedData(context, userManager); } catch (Exception e) { diff --git a/API/Services/Auth/TokenService.cs b/API/Services/Auth/TokenService.cs new file mode 100644 index 0000000..a52f1aa --- /dev/null +++ b/API/Services/Auth/TokenService.cs @@ -0,0 +1,36 @@ +using System.IdentityModel.Tokens.Jwt; +using System.Security.Claims; +using System.Text; +using Domain.Entities; +using Microsoft.IdentityModel.Tokens; + +namespace API.Services.Auth; + +public class TokenService(IConfiguration config) +{ + public string CreateToken(User user) + { + var claims = new List + { + new(ClaimTypes.Name, user.UserName), + new(ClaimTypes.NameIdentifier, user.Id), + new(ClaimTypes.Email, user.Email) + }; + + var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(config["TokenKey"])); + var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha512Signature); + + var tokenDescriptor = new SecurityTokenDescriptor + { + Subject = new ClaimsIdentity(claims), + Expires = DateTime.UtcNow.AddDays(7), + SigningCredentials = creds + }; + + var tokenHandler = new JwtSecurityTokenHandler(); + + var token = tokenHandler.CreateToken(tokenDescriptor); + + return tokenHandler.WriteToken(token); + } +} diff --git a/API/activity-hub.db b/API/activity-hub.db index dc5d484f0f8bcb46cb519296ac80038982236975..48053e2729b272ecd1f7e9eb93972715e02e0a99 100644 GIT binary patch delta 244 zcmZozz}T>WaRZBi6gQVC6Ms41Q@(iKN4)+#`*?J?H*!mI^>CSPY!v4T59Q_v6*U%> z6csfvw{UVaH!;*TH8nKUHL)~u)U|Xov(Pm$HF9z`a5Fb`H8LzoEiN%KFf!3Ku+TL! zRxmQKG5{fva7s~TUUo5v1;U$qtW+7f$~ibfb*;@MC50#N^;4OA-z6>l2u?rarjIn`m3bTwlB>@U~ z2nbaxK{pv06O+A8B$MAe4;eBrGBhnPHZ3tYATcsJG%-3cFfKJWG&MGp1w19QEIil) V1PTsk0J91}G!BuFE3;=#nE@>rA0Pk# diff --git a/API/activity-hub.db-shm b/API/activity-hub.db-shm index 5a96818a44c42d4f8019f2ba6f4a1f32e5cb8dfa..d7a8950c8d485c56f97e271b3c7ce5fde00cd46d 100644 GIT binary patch literal 32768 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 OIpFgqvBKZw3%mhOOx$V! delta 168 zcmX>wOZi1Yhnps^KZ8CpDi=PA6^3qvH&qFOg!dwM&$cP zPmFnif?QlZO#J11Px<0`AMyJ0?Bmhl-pDP<)w6k`K>^qHGy9mzS!F?*;fB^Pf3CYS quPX+mnVZX$X}iZ3rvLmBaKpI^J(LdpmE__A$@AH5k3YinlOF)m**JXw diff --git a/API/appsettings.Development.json b/API/appsettings.Development.json index 4be8d50..9bd68a8 100644 --- a/API/appsettings.Development.json +++ b/API/appsettings.Development.json @@ -7,5 +7,6 @@ }, "ConnectionStrings": { "DefaultConnection": "Data Source=activity-hub.db" - } + }, + "TokenKey": "887aa8d55236124da3ae8da02e0809d2140470eb9aa6583686fafed91b9586eb" } diff --git a/Application/Core/Result.cs b/Application/Core/Result.cs index e2696a2..4290b16 100644 --- a/Application/Core/Result.cs +++ b/Application/Core/Result.cs @@ -8,11 +8,11 @@ public class Result public static Result Success(T value) { - return new Result() { IsSuccess = true, Value = value }; + return new Result { IsSuccess = true, Value = value }; } public static Result Failure(string error) { - return new Result() { IsSuccess = false, Error = error }; + return new Result { IsSuccess = false, Error = error }; } } diff --git a/Domain/Domain.csproj b/Domain/Domain.csproj index 2292000..70cb6fe 100644 --- a/Domain/Domain.csproj +++ b/Domain/Domain.csproj @@ -6,4 +6,8 @@ disable + + + + diff --git a/Domain/Entities/User.cs b/Domain/Entities/User.cs new file mode 100644 index 0000000..3ed7994 --- /dev/null +++ b/Domain/Entities/User.cs @@ -0,0 +1,9 @@ +using Microsoft.AspNetCore.Identity; + +namespace Domain.Entities; + +public class User : IdentityUser +{ + public string DisplayName { get; set; } + public string Bio { get; set; } +} diff --git a/Persistence/DataContext.cs b/Persistence/DataContext.cs index 2888a52..84ec6cb 100644 --- a/Persistence/DataContext.cs +++ b/Persistence/DataContext.cs @@ -1,9 +1,10 @@ using Domain.Entities; +using Microsoft.AspNetCore.Identity.EntityFrameworkCore; using Microsoft.EntityFrameworkCore; namespace Persistence; -public class DataContext(DbContextOptions options) : DbContext(options) +public class DataContext(DbContextOptions options) : IdentityDbContext(options) { public DbSet Activities { get; set; } } diff --git a/Persistence/Migrations/20240805112415_InitialCreate.Designer.cs b/Persistence/Migrations/20240805112415_InitialCreate.Designer.cs deleted file mode 100644 index 04bdf9d..0000000 --- a/Persistence/Migrations/20240805112415_InitialCreate.Designer.cs +++ /dev/null @@ -1,54 +0,0 @@ -// -using System; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -using Persistence; - -#nullable disable - -namespace Persistence.Migrations -{ - [DbContext(typeof(DataContext))] - [Migration("20240805112415_InitialCreate")] - partial class InitialCreate - { - /// - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder.HasAnnotation("ProductVersion", "8.0.7"); - - modelBuilder.Entity("Domain.features.activity.Activity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("TEXT"); - - b.Property("Category") - .HasColumnType("TEXT"); - - b.Property("City") - .HasColumnType("TEXT"); - - b.Property("Date") - .HasColumnType("TEXT"); - - b.Property("Description") - .HasColumnType("TEXT"); - - b.Property("Title") - .HasColumnType("TEXT"); - - b.Property("Venue") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.ToTable("Activities"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/Persistence/Migrations/20240805112415_InitialCreate.cs b/Persistence/Migrations/20240805112415_InitialCreate.cs deleted file mode 100644 index 04f8673..0000000 --- a/Persistence/Migrations/20240805112415_InitialCreate.cs +++ /dev/null @@ -1,39 +0,0 @@ -using System; -using Microsoft.EntityFrameworkCore.Migrations; - -#nullable disable - -namespace Persistence.Migrations -{ - /// - public partial class InitialCreate : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.CreateTable( - name: "Activities", - columns: table => new - { - Id = table.Column(type: "TEXT", nullable: false), - Title = table.Column(type: "TEXT", nullable: true), - Date = table.Column(type: "TEXT", nullable: false), - Description = table.Column(type: "TEXT", nullable: true), - Category = table.Column(type: "TEXT", nullable: true), - City = table.Column(type: "TEXT", nullable: true), - Venue = table.Column(type: "TEXT", nullable: true) - }, - constraints: table => - { - table.PrimaryKey("PK_Activities", x => x.Id); - }); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropTable( - name: "Activities"); - } - } -} diff --git a/Persistence/Migrations/20240820065605_InitialCreate.Designer.cs b/Persistence/Migrations/20240820065605_InitialCreate.Designer.cs new file mode 100644 index 0000000..ef127ec --- /dev/null +++ b/Persistence/Migrations/20240820065605_InitialCreate.Designer.cs @@ -0,0 +1,303 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Persistence; + +#nullable disable + +namespace Persistence.Migrations +{ + [DbContext(typeof(DataContext))] + [Migration("20240820065605_InitialCreate")] + partial class InitialCreate + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "8.0.8"); + + modelBuilder.Entity("Domain.Entities.Activity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Category") + .HasColumnType("TEXT"); + + b.Property("City") + .HasColumnType("TEXT"); + + b.Property("Date") + .HasColumnType("TEXT"); + + b.Property("Description") + .HasColumnType("TEXT"); + + b.Property("Title") + .HasColumnType("TEXT"); + + b.Property("Venue") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("Activities"); + }); + + modelBuilder.Entity("Domain.Entities.User", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AccessFailedCount") + .HasColumnType("INTEGER"); + + b.Property("Bio") + .HasColumnType("TEXT"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("TEXT"); + + b.Property("DisplayName") + .HasColumnType("TEXT"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("EmailConfirmed") + .HasColumnType("INTEGER"); + + b.Property("LockoutEnabled") + .HasColumnType("INTEGER"); + + b.Property("LockoutEnd") + .HasColumnType("TEXT"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("PasswordHash") + .HasColumnType("TEXT"); + + b.Property("PhoneNumber") + .HasColumnType("TEXT"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("INTEGER"); + + b.Property("SecurityStamp") + .HasColumnType("TEXT"); + + b.Property("TwoFactorEnabled") + .HasColumnType("INTEGER"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasDatabaseName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex"); + + b.ToTable("AspNetUsers", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("TEXT"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("NormalizedName") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasDatabaseName("RoleNameIndex"); + + b.ToTable("AspNetRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ClaimType") + .HasColumnType("TEXT"); + + b.Property("ClaimValue") + .HasColumnType("TEXT"); + + b.Property("RoleId") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ClaimType") + .HasColumnType("TEXT"); + + b.Property("ClaimValue") + .HasColumnType("TEXT"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider") + .HasColumnType("TEXT"); + + b.Property("ProviderKey") + .HasColumnType("TEXT"); + + b.Property("ProviderDisplayName") + .HasColumnType("TEXT"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.Property("UserId") + .HasColumnType("TEXT"); + + b.Property("RoleId") + .HasColumnType("TEXT"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("TEXT"); + + b.Property("LoginProvider") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Value") + .HasColumnType("TEXT"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens", (string)null); + }); + + 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("Domain.Entities.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("Domain.Entities.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("Domain.Entities.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("Domain.Entities.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Persistence/Migrations/20240820065605_InitialCreate.cs b/Persistence/Migrations/20240820065605_InitialCreate.cs new file mode 100644 index 0000000..09c0dd7 --- /dev/null +++ b/Persistence/Migrations/20240820065605_InitialCreate.cs @@ -0,0 +1,244 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Persistence.Migrations +{ + /// + public partial class InitialCreate : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "Activities", + columns: table => new + { + Id = table.Column(type: "TEXT", nullable: false), + Title = table.Column(type: "TEXT", nullable: true), + Date = table.Column(type: "TEXT", nullable: false), + Description = table.Column(type: "TEXT", nullable: true), + Category = table.Column(type: "TEXT", nullable: true), + City = table.Column(type: "TEXT", nullable: true), + Venue = table.Column(type: "TEXT", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Activities", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "AspNetRoles", + columns: table => new + { + Id = table.Column(type: "TEXT", nullable: false), + Name = table.Column(type: "TEXT", maxLength: 256, nullable: true), + NormalizedName = table.Column(type: "TEXT", maxLength: 256, nullable: true), + ConcurrencyStamp = table.Column(type: "TEXT", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetRoles", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "AspNetUsers", + columns: table => new + { + Id = table.Column(type: "TEXT", nullable: false), + DisplayName = table.Column(type: "TEXT", nullable: true), + Bio = table.Column(type: "TEXT", nullable: true), + UserName = table.Column(type: "TEXT", maxLength: 256, nullable: true), + NormalizedUserName = table.Column(type: "TEXT", maxLength: 256, nullable: true), + Email = table.Column(type: "TEXT", maxLength: 256, nullable: true), + NormalizedEmail = table.Column(type: "TEXT", maxLength: 256, nullable: true), + EmailConfirmed = table.Column(type: "INTEGER", nullable: false), + PasswordHash = table.Column(type: "TEXT", nullable: true), + SecurityStamp = table.Column(type: "TEXT", nullable: true), + ConcurrencyStamp = table.Column(type: "TEXT", nullable: true), + PhoneNumber = table.Column(type: "TEXT", nullable: true), + PhoneNumberConfirmed = table.Column(type: "INTEGER", nullable: false), + TwoFactorEnabled = table.Column(type: "INTEGER", nullable: false), + LockoutEnd = table.Column(type: "TEXT", nullable: true), + LockoutEnabled = table.Column(type: "INTEGER", nullable: false), + AccessFailedCount = table.Column(type: "INTEGER", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetUsers", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "AspNetRoleClaims", + columns: table => new + { + Id = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + RoleId = table.Column(type: "TEXT", nullable: false), + ClaimType = table.Column(type: "TEXT", nullable: true), + ClaimValue = table.Column(type: "TEXT", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetRoleClaims", x => x.Id); + table.ForeignKey( + name: "FK_AspNetRoleClaims_AspNetRoles_RoleId", + column: x => x.RoleId, + principalTable: "AspNetRoles", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "AspNetUserClaims", + columns: table => new + { + Id = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + UserId = table.Column(type: "TEXT", nullable: false), + ClaimType = table.Column(type: "TEXT", nullable: true), + ClaimValue = table.Column(type: "TEXT", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetUserClaims", x => x.Id); + table.ForeignKey( + name: "FK_AspNetUserClaims_AspNetUsers_UserId", + column: x => x.UserId, + principalTable: "AspNetUsers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "AspNetUserLogins", + columns: table => new + { + LoginProvider = table.Column(type: "TEXT", nullable: false), + ProviderKey = table.Column(type: "TEXT", nullable: false), + ProviderDisplayName = table.Column(type: "TEXT", nullable: true), + UserId = table.Column(type: "TEXT", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetUserLogins", x => new { x.LoginProvider, x.ProviderKey }); + table.ForeignKey( + name: "FK_AspNetUserLogins_AspNetUsers_UserId", + column: x => x.UserId, + principalTable: "AspNetUsers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "AspNetUserRoles", + columns: table => new + { + UserId = table.Column(type: "TEXT", nullable: false), + RoleId = table.Column(type: "TEXT", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetUserRoles", x => new { x.UserId, x.RoleId }); + table.ForeignKey( + name: "FK_AspNetUserRoles_AspNetRoles_RoleId", + column: x => x.RoleId, + principalTable: "AspNetRoles", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_AspNetUserRoles_AspNetUsers_UserId", + column: x => x.UserId, + principalTable: "AspNetUsers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "AspNetUserTokens", + columns: table => new + { + UserId = table.Column(type: "TEXT", nullable: false), + LoginProvider = table.Column(type: "TEXT", nullable: false), + Name = table.Column(type: "TEXT", nullable: false), + Value = table.Column(type: "TEXT", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetUserTokens", x => new { x.UserId, x.LoginProvider, x.Name }); + table.ForeignKey( + name: "FK_AspNetUserTokens_AspNetUsers_UserId", + column: x => x.UserId, + principalTable: "AspNetUsers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_AspNetRoleClaims_RoleId", + table: "AspNetRoleClaims", + column: "RoleId"); + + migrationBuilder.CreateIndex( + name: "RoleNameIndex", + table: "AspNetRoles", + column: "NormalizedName", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_AspNetUserClaims_UserId", + table: "AspNetUserClaims", + column: "UserId"); + + migrationBuilder.CreateIndex( + name: "IX_AspNetUserLogins_UserId", + table: "AspNetUserLogins", + column: "UserId"); + + migrationBuilder.CreateIndex( + name: "IX_AspNetUserRoles_RoleId", + table: "AspNetUserRoles", + column: "RoleId"); + + migrationBuilder.CreateIndex( + name: "EmailIndex", + table: "AspNetUsers", + column: "NormalizedEmail"); + + migrationBuilder.CreateIndex( + name: "UserNameIndex", + table: "AspNetUsers", + column: "NormalizedUserName", + unique: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "Activities"); + + migrationBuilder.DropTable( + name: "AspNetRoleClaims"); + + migrationBuilder.DropTable( + name: "AspNetUserClaims"); + + migrationBuilder.DropTable( + name: "AspNetUserLogins"); + + migrationBuilder.DropTable( + name: "AspNetUserRoles"); + + migrationBuilder.DropTable( + name: "AspNetUserTokens"); + + migrationBuilder.DropTable( + name: "AspNetRoles"); + + migrationBuilder.DropTable( + name: "AspNetUsers"); + } + } +} diff --git a/Persistence/Migrations/DataContextModelSnapshot.cs b/Persistence/Migrations/DataContextModelSnapshot.cs index 228347e..bcfb0f2 100644 --- a/Persistence/Migrations/DataContextModelSnapshot.cs +++ b/Persistence/Migrations/DataContextModelSnapshot.cs @@ -15,9 +15,9 @@ partial class DataContextModelSnapshot : ModelSnapshot protected override void BuildModel(ModelBuilder modelBuilder) { #pragma warning disable 612, 618 - modelBuilder.HasAnnotation("ProductVersion", "8.0.7"); + modelBuilder.HasAnnotation("ProductVersion", "8.0.8"); - modelBuilder.Entity("Domain.features.activity.Activity", b => + modelBuilder.Entity("Domain.Entities.Activity", b => { b.Property("Id") .ValueGeneratedOnAdd() @@ -45,6 +45,255 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("Activities"); }); + + modelBuilder.Entity("Domain.Entities.User", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AccessFailedCount") + .HasColumnType("INTEGER"); + + b.Property("Bio") + .HasColumnType("TEXT"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("TEXT"); + + b.Property("DisplayName") + .HasColumnType("TEXT"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("EmailConfirmed") + .HasColumnType("INTEGER"); + + b.Property("LockoutEnabled") + .HasColumnType("INTEGER"); + + b.Property("LockoutEnd") + .HasColumnType("TEXT"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("PasswordHash") + .HasColumnType("TEXT"); + + b.Property("PhoneNumber") + .HasColumnType("TEXT"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("INTEGER"); + + b.Property("SecurityStamp") + .HasColumnType("TEXT"); + + b.Property("TwoFactorEnabled") + .HasColumnType("INTEGER"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasDatabaseName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex"); + + b.ToTable("AspNetUsers", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("TEXT"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("NormalizedName") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasDatabaseName("RoleNameIndex"); + + b.ToTable("AspNetRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ClaimType") + .HasColumnType("TEXT"); + + b.Property("ClaimValue") + .HasColumnType("TEXT"); + + b.Property("RoleId") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ClaimType") + .HasColumnType("TEXT"); + + b.Property("ClaimValue") + .HasColumnType("TEXT"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider") + .HasColumnType("TEXT"); + + b.Property("ProviderKey") + .HasColumnType("TEXT"); + + b.Property("ProviderDisplayName") + .HasColumnType("TEXT"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.Property("UserId") + .HasColumnType("TEXT"); + + b.Property("RoleId") + .HasColumnType("TEXT"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("TEXT"); + + b.Property("LoginProvider") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Value") + .HasColumnType("TEXT"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens", (string)null); + }); + + 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("Domain.Entities.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("Domain.Entities.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("Domain.Entities.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("Domain.Entities.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); #pragma warning restore 612, 618 } } diff --git a/Persistence/Persistence.csproj b/Persistence/Persistence.csproj index 576cfcf..94f0e4a 100644 --- a/Persistence/Persistence.csproj +++ b/Persistence/Persistence.csproj @@ -5,7 +5,11 @@ - + + + + + diff --git a/Persistence/Seed.cs b/Persistence/Seed.cs index 3775928..d12599d 100644 --- a/Persistence/Seed.cs +++ b/Persistence/Seed.cs @@ -1,11 +1,24 @@ using Domain.Entities; +using Microsoft.AspNetCore.Identity; namespace Persistence; public class Seed { - public static async Task SeedData(DataContext context) + public static async Task SeedData(DataContext context, UserManager userManager) { + if (!userManager.Users.Any()) + { + var users = new List + { + new() { DisplayName = "Bob", UserName = "bob", Email = "bob@test.com" }, + new() { DisplayName = "Tom", UserName = "tom", Email = "tom@test.com" }, + new() { DisplayName = "Jane", UserName = "jane", Email = "jane@test.com" } + }; + + foreach (var user in users) await userManager.CreateAsync(user, "Pa$$w0rd"); + } + if (context.Activities.Any()) return; var activities = new List From 5949cd0f5e91866bf8899588ca078ab22bfd7849 Mon Sep 17 00:00:00 2001 From: Alper Soy <51034169+Alper-Soy@users.noreply.github.com> Date: Tue, 20 Aug 2024 17:27:42 +0300 Subject: [PATCH 2/2] AH-13 client side login and register (#18) --- API/Controllers/AccountController.cs | 12 +++- API/activity-hub.db | Bin 20480 -> 110592 bytes API/activity-hub.db-shm | Bin 32768 -> 32768 bytes API/activity-hub.db-wal | Bin 576832 -> 53592 bytes client-app/src/app/api/agent.ts | 19 +++++- client-app/src/app/common/form/TextInput.tsx | 1 + .../src/app/common/modals/ModalContainer.tsx | 16 +++++ client-app/src/app/layout/App.tsx | 17 +++++ client-app/src/app/layout/NavBar.tsx | 33 +++++++-- client-app/src/app/models/user.ts | 13 ++++ client-app/src/app/router/Routes.tsx | 2 + client-app/src/app/stores/commonStore.ts | 23 ++++++- client-app/src/app/stores/modalStore.ts | 27 ++++++++ client-app/src/app/stores/store.ts | 6 ++ client-app/src/app/stores/userStore.ts | 48 +++++++++++++ .../dashboard/ActivityDashboard.tsx | 2 +- client-app/src/features/home/HomePage.tsx | 45 ++++++++++-- client-app/src/features/users/LoginForm.tsx | 50 ++++++++++++++ .../src/features/users/RegisterForm.tsx | 64 ++++++++++++++++++ 19 files changed, 361 insertions(+), 17 deletions(-) create mode 100644 client-app/src/app/common/modals/ModalContainer.tsx create mode 100644 client-app/src/app/models/user.ts create mode 100644 client-app/src/app/stores/modalStore.ts create mode 100644 client-app/src/app/stores/userStore.ts create mode 100644 client-app/src/features/users/LoginForm.tsx create mode 100644 client-app/src/features/users/RegisterForm.tsx diff --git a/API/Controllers/AccountController.cs b/API/Controllers/AccountController.cs index 2c42ea0..9f7e1c8 100644 --- a/API/Controllers/AccountController.cs +++ b/API/Controllers/AccountController.cs @@ -34,10 +34,18 @@ public async Task> Login(LoginDto loginDto) public async Task> Register(RegisterDto registerDto) { if (await userManager.Users.AnyAsync(x => x.UserName == registerDto.Username)) - return BadRequest("Username is already taken"); + { + ModelState.AddModelError("username", "Username taken"); + return ValidationProblem(); + } + if (await userManager.Users.AnyAsync(x => x.Email == registerDto.Email)) - return BadRequest("Email is already taken"); + { + ModelState.AddModelError("email", "Email taken"); + return ValidationProblem(); + } + var user = new User { diff --git a/API/activity-hub.db b/API/activity-hub.db index 48053e2729b272ecd1f7e9eb93972715e02e0a99..fc9d37143355817f7f66080df6af06bd5d37de4d 100644 GIT binary patch literal 110592 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 Sa=w>jA~N~uY%-tpQv4tH_g$<2 literal 20480 zcmeI3O>f&a7{~pR7sqMr4nq(a#V|G-uof*-i4sMr1&UFWv}MiPQoC3Wg`p-&BFvUL zTW(kEln&Ubhkb;7fnnHjmwk%?!%jniUG@of9Od`MPUk}}!5%?`De58l=kIx_B#)f? zpKpapfR5r;(@&6=xtz)6G9M$9$z*b}%*!%3((Adwh5XEp)z7QuG8;#Kuay7E%&ol4 zlzuD!xbm{}!+FNg4g`Pz5C8%|00;m9AOHk_01)`U5P0!UesS&U)$F&;#DClf_(2jr z4U;fvk7{#b-{n;oRk^+8qV>^@b#$$WECXHY)g6!RIcKl@6+8=jtCukqE!Y4@>N0Z%z>~?GCR17;1W{)=kDR@DU7K52`{Zgi6Gn%@*X^$w@{iTJ-$~-`_1IeK>S$f_r?u?j z8e`cPn_ZCgy6bI+$1Oh<+5R+aCsNYX#@y?8r`ilj4~P6uyLO*Oe{*nUNNoO~bNAMyMG_ z$w?c95jyG)(9^ILM~895qc}MUTIfmV@#*SPDu!RFST?gYO>+sRo@HZQ@f6Gz+rf(M zQo>BZ6snvRqxM#8O%Y=Z#i%_oBP|D=M!Ke+m>pl%{RZlPVHs_A+CiuJtWc=oJFHT9 zmve`uC#D*4u+E8!xnWSOSi0(2n&NTO-1FPXh>&~>*!6>vs#2^GqM;%(z~djs<6!nW zk0p}wV&6ZQo3<)AbEH>Xj&(v<>K_kVrlT0tvsK+DXOU1jxl}jlAEvUQ06Gvb)jywg z8j!G~WLORJY97Pch*KPf*BE1(eWWq^I9XUyVqoRkw z>w+%Nr-Gz=?pT@xB3#MDa-@4~RZ0_m2B%lnDoijTBULJ*x`K6KNJSPAn7Wo@sjlY{ z&v-pxNH1R4gdW2j${7T)jF^7-?)j~-y%l$!wt~}qzU!dXN<}^jG)Lz)_DowoAY37^ zX}U~8p+dqCoNLxu9%he+J~2NdBL|ri4mt`O%^njGMnTg*E$6x_)+)Dj)z+BGbxfHp z8R){4&lDMx!t)fS+f<{H!kGt)(Ni%cOvvEB4xS@pLPfLF4i9=V_Tx@;coNGEjD+7j z45HJ8Y+uoPnkzga_Y<~VMpH#DHVsCw<=Vt`sYO)NIIF1V7gd?yVpt=`4C7?DmBtk9 z62x0Uk_=(l4d&OUG~b#|DfB1b_e#00KY&2mk>f z00e*l5C8%fmB6)pEjvpJa5*)cC8t+R4QI)>Ev1IDWWWkt!x_?5i(SJRvPcW5;Vjvm e`P6Whq|01tI7@ycpBm1R!pMD;uPx1zS@;)^EcGG) diff --git a/API/activity-hub.db-shm b/API/activity-hub.db-shm index d7a8950c8d485c56f97e271b3c7ce5fde00cd46d..d6d7fdfcf9b3c22ffae353bd3cd002bb4b25eb42 100644 GIT binary patch delta 202 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 OIpFgqvBKZw3%mhOOx$V! diff --git a/client-app/src/app/api/agent.ts b/client-app/src/app/api/agent.ts index e0d18ab..bb354b7 100644 --- a/client-app/src/app/api/agent.ts +++ b/client-app/src/app/api/agent.ts @@ -3,6 +3,7 @@ import { toast } from 'react-toastify'; import { Activity } from '../models/activity'; import { router } from '../router/Routes'; import { store } from '../stores/store'; +import { User, UserFormValues } from '../models/user'; const sleep = (delay: number) => { return new Promise((resolve) => setTimeout(resolve, delay)); @@ -10,6 +11,14 @@ const sleep = (delay: number) => { axios.defaults.baseURL = 'http://localhost:5000/api'; +const responseBody = (response: AxiosResponse) => response.data; + +axios.interceptors.request.use((config) => { + const token = store.commonStore.token; + if (token && config.headers) config.headers.Authorization = `Bearer ${token}`; + return config; +}); + axios.interceptors.response.use( async (response) => { await sleep(1000); @@ -55,8 +64,6 @@ axios.interceptors.response.use( } ); -const responseBody = (response: AxiosResponse) => response.data; - const requests = { get: (url: string) => axios.get(url).then(responseBody), post: (url: string, body: object) => @@ -75,8 +82,16 @@ const Activities = { delete: (id: string) => requests.del(`/activities/${id}`), }; +const Account = { + current: () => requests.get('account'), + login: (user: UserFormValues) => requests.post('/account/login', user), + register: (user: UserFormValues) => + requests.post('/account/register', user), +}; + const agent = { Activities, + Account, }; export default agent; diff --git a/client-app/src/app/common/form/TextInput.tsx b/client-app/src/app/common/form/TextInput.tsx index 85824e9..c9d78d5 100644 --- a/client-app/src/app/common/form/TextInput.tsx +++ b/client-app/src/app/common/form/TextInput.tsx @@ -5,6 +5,7 @@ interface Props { placeholder: string; name: string; label?: string; + type?: string; } export default function TextInput(props: Props) { diff --git a/client-app/src/app/common/modals/ModalContainer.tsx b/client-app/src/app/common/modals/ModalContainer.tsx new file mode 100644 index 0000000..6fe36d5 --- /dev/null +++ b/client-app/src/app/common/modals/ModalContainer.tsx @@ -0,0 +1,16 @@ +import { observer } from 'mobx-react-lite'; +import { Modal } from 'semantic-ui-react'; +import { useStore } from '../../stores/store'; + +export default observer(function ModalContainer() { + const { modalStore } = useStore(); + return ( + + {modalStore.modal.body} + + ); +}); diff --git a/client-app/src/app/layout/App.tsx b/client-app/src/app/layout/App.tsx index 948033f..bfc7f68 100644 --- a/client-app/src/app/layout/App.tsx +++ b/client-app/src/app/layout/App.tsx @@ -4,12 +4,29 @@ import { observer } from 'mobx-react-lite'; import { Outlet, useLocation } from 'react-router-dom'; import HomePage from '../../features/home/HomePage'; import { ToastContainer } from 'react-toastify'; +import { useStore } from '../stores/store'; +import { useEffect } from 'react'; +import LoadingComponent from './LoadingComponent'; +import ModalContainer from '../common/modals/ModalContainer'; function App() { const location = useLocation(); + const { commonStore, userStore } = useStore(); + + useEffect(() => { + if (commonStore.token) { + userStore.getUser().finally(() => commonStore.setAppLoaded()); + } else { + commonStore.setAppLoaded(); + } + }, [commonStore, userStore]); + + if (!commonStore.appLoaded) + return ; return ( <> + {location.pathname === '/' ? ( diff --git a/client-app/src/app/layout/NavBar.tsx b/client-app/src/app/layout/NavBar.tsx index e244d27..a02a840 100644 --- a/client-app/src/app/layout/NavBar.tsx +++ b/client-app/src/app/layout/NavBar.tsx @@ -1,7 +1,12 @@ -import { Button, Container, Menu } from 'semantic-ui-react'; -import { NavLink } from 'react-router-dom'; +import { observer } from 'mobx-react-lite'; +import { useStore } from '../stores/store'; +import { Button, Container, Dropdown, Menu, Image } from 'semantic-ui-react'; +import { Link, NavLink } from 'react-router-dom'; -export default function NavBar() { +export default observer(function NavBar() { + const { + userStore: { user, logout }, + } = useStore(); return ( @@ -10,7 +15,7 @@ export default function NavBar() { Activity Hub - + ); -} +}); diff --git a/client-app/src/app/models/user.ts b/client-app/src/app/models/user.ts new file mode 100644 index 0000000..109285d --- /dev/null +++ b/client-app/src/app/models/user.ts @@ -0,0 +1,13 @@ +export interface User { + username: string; + displayName: string; + token: string; + image?: string; +} + +export interface UserFormValues { + email: string; + password: string; + displayName?: string; + username?: string; +} diff --git a/client-app/src/app/router/Routes.tsx b/client-app/src/app/router/Routes.tsx index 7de8b4c..8b5f4dd 100644 --- a/client-app/src/app/router/Routes.tsx +++ b/client-app/src/app/router/Routes.tsx @@ -6,6 +6,7 @@ import ActivityDetails from '../../features/activities/details/ActivityDetails'; import TestErrors from '../../features/errors/TestError'; import NotFound from '../../features/errors/NotFound'; import ServerError from '../../features/errors/ServerError'; +import LoginForm from '../../features/users/LoginForm'; export const routes: RouteObject[] = [ { @@ -16,6 +17,7 @@ export const routes: RouteObject[] = [ { path: 'activities/:id', element: }, { path: 'createActivity', element: }, { path: 'manage/:id', element: }, + { path: 'login', element: }, { path: 'errors', element: }, { path: 'not-found', element: }, { path: 'server-error', element: }, diff --git a/client-app/src/app/stores/commonStore.ts b/client-app/src/app/stores/commonStore.ts index f8d44e6..57f3d8e 100644 --- a/client-app/src/app/stores/commonStore.ts +++ b/client-app/src/app/stores/commonStore.ts @@ -1,14 +1,35 @@ -import { makeAutoObservable } from 'mobx'; +import { makeAutoObservable, reaction } from 'mobx'; import { ServerError } from '../models/serverError'; export default class CommonStore { error: ServerError | null = null; + token: string | null = localStorage.getItem('jwt'); + appLoaded = false; constructor() { makeAutoObservable(this); + + reaction( + () => this.token, + (token) => { + if (token) { + localStorage.setItem('jwt', token); + } else { + localStorage.removeItem('jwt'); + } + } + ); } setServerError(error: ServerError) { this.error = error; } + + setToken = (token: string | null) => { + this.token = token; + }; + + setAppLoaded = () => { + this.appLoaded = true; + }; } diff --git a/client-app/src/app/stores/modalStore.ts b/client-app/src/app/stores/modalStore.ts new file mode 100644 index 0000000..e18cce6 --- /dev/null +++ b/client-app/src/app/stores/modalStore.ts @@ -0,0 +1,27 @@ +import { makeAutoObservable } from 'mobx'; + +interface Modal { + open: boolean; + body: JSX.Element | null; +} + +export default class ModalStore { + modal: Modal = { + open: false, + body: null, + }; + + constructor() { + makeAutoObservable(this); + } + + openModal = (content: JSX.Element) => { + this.modal.open = true; + this.modal.body = content; + }; + + closeModal = () => { + this.modal.open = false; + this.modal.body = null; + }; +} diff --git a/client-app/src/app/stores/store.ts b/client-app/src/app/stores/store.ts index 2838028..4285f1d 100644 --- a/client-app/src/app/stores/store.ts +++ b/client-app/src/app/stores/store.ts @@ -1,15 +1,21 @@ import ActivityStore from './activityStore'; import { createContext, useContext } from 'react'; import CommonStore from './commonStore'; +import UserStore from './userStore'; +import ModalStore from './modalStore'; interface Store { activityStore: ActivityStore; commonStore: CommonStore; + userStore: UserStore; + modalStore: ModalStore; } export const store: Store = { activityStore: new ActivityStore(), commonStore: new CommonStore(), + userStore: new UserStore(), + modalStore: new ModalStore(), }; export const StoreContext = createContext(store); diff --git a/client-app/src/app/stores/userStore.ts b/client-app/src/app/stores/userStore.ts new file mode 100644 index 0000000..67b3efe --- /dev/null +++ b/client-app/src/app/stores/userStore.ts @@ -0,0 +1,48 @@ +import { makeAutoObservable, runInAction } from 'mobx'; +import agent from '../api/agent'; +import { User, UserFormValues } from '../models/user'; +import { router } from '../router/Routes'; +import { store } from './store'; + +export default class UserStore { + user: User | null = null; + + constructor() { + makeAutoObservable(this); + } + + get isLoggedIn() { + return !!this.user; + } + + login = async (creds: UserFormValues) => { + const user = await agent.Account.login(creds); + store.commonStore.setToken(user.token); + runInAction(() => (this.user = user)); + await router.navigate('/activities'); + store.modalStore.closeModal(); + }; + + register = async (creds: UserFormValues) => { + const user = await agent.Account.register(creds); + store.commonStore.setToken(user.token); + runInAction(() => (this.user = user)); + await router.navigate('/activities'); + store.modalStore.closeModal(); + }; + + logout = async () => { + store.commonStore.setToken(null); + this.user = null; + await router.navigate('/'); + }; + + getUser = async () => { + try { + const user = await agent.Account.current(); + runInAction(() => (this.user = user)); + } catch (error) { + console.log(error); + } + }; +} diff --git a/client-app/src/features/activities/dashboard/ActivityDashboard.tsx b/client-app/src/features/activities/dashboard/ActivityDashboard.tsx index 1679a3f..cf30478 100644 --- a/client-app/src/features/activities/dashboard/ActivityDashboard.tsx +++ b/client-app/src/features/activities/dashboard/ActivityDashboard.tsx @@ -15,7 +15,7 @@ export default observer(function ActivityDashboard() { }, [activityRegistry.size, loadActivities]); if (activityStore.loadingInitial) - return ; + return ; return ( diff --git a/client-app/src/features/home/HomePage.tsx b/client-app/src/features/home/HomePage.tsx index 5db559e..20f0510 100644 --- a/client-app/src/features/home/HomePage.tsx +++ b/client-app/src/features/home/HomePage.tsx @@ -1,7 +1,12 @@ +import { observer } from 'mobx-react-lite'; import { Link } from 'react-router-dom'; import { Button, Container, Header, Segment, Image } from 'semantic-ui-react'; +import { useStore } from '../../app/stores/store'; +import LoginForm from '../users/LoginForm'; +import RegisterForm from '../users/RegisterForm'; -export default function HomePage() { +export default observer(function HomePage() { + const { userStore, modalStore } = useStore(); return ( @@ -14,11 +19,39 @@ export default function HomePage() { /> Activity Hub -
- + {!userStore.isLoggedIn && ( +
+ )} + {userStore.isLoggedIn ? ( + <> +
+ + + ) : ( + <> + + + + )} ); -} +}); diff --git a/client-app/src/features/users/LoginForm.tsx b/client-app/src/features/users/LoginForm.tsx new file mode 100644 index 0000000..9a7bff2 --- /dev/null +++ b/client-app/src/features/users/LoginForm.tsx @@ -0,0 +1,50 @@ +import { ErrorMessage, Form, Formik } from 'formik'; +import { observer } from 'mobx-react-lite'; +import { Button, Header, Label } from 'semantic-ui-react'; +import { useStore } from '../../app/stores/store'; +import TextInput from '../../app/common/form/TextInput'; + +export default observer(function LoginForm() { + const { userStore } = useStore(); + return ( + + userStore + .login(values) + .catch(() => setErrors({ error: 'Invalid email or password' })) + } + > + {({ handleSubmit, isSubmitting, errors }) => ( +
+
+ + + ( +