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..9f7e1c8
--- /dev/null
+++ b/API/Controllers/AccountController.cs
@@ -0,0 +1,83 @@
+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))
+ {
+ ModelState.AddModelError("username", "Username taken");
+ return ValidationProblem();
+ }
+
+
+ if (await userManager.Users.AnyAsync(x => x.Email == registerDto.Email))
+ {
+ ModelState.AddModelError("email", "Email taken");
+ return ValidationProblem();
+ }
+
+
+ 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 dc5d484..fc9d371 100644
Binary files a/API/activity-hub.db and b/API/activity-hub.db differ
diff --git a/API/activity-hub.db-shm b/API/activity-hub.db-shm
index 5a96818..d6d7fdf 100644
Binary files a/API/activity-hub.db-shm and b/API/activity-hub.db-shm differ
diff --git a/API/activity-hub.db-wal b/API/activity-hub.db-wal
index 1195991..b264658 100644
Binary files a/API/activity-hub.db-wal and b/API/activity-hub.db-wal differ
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
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 (
);
-}
+});
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 }) => (
+
+ )}
+
+ );
+});
diff --git a/client-app/src/features/users/RegisterForm.tsx b/client-app/src/features/users/RegisterForm.tsx
new file mode 100644
index 0000000..62d09de
--- /dev/null
+++ b/client-app/src/features/users/RegisterForm.tsx
@@ -0,0 +1,64 @@
+import { ErrorMessage, Form, Formik } from 'formik';
+import { observer } from 'mobx-react-lite';
+import { Button, Header } from 'semantic-ui-react';
+import { useStore } from '../../app/stores/store';
+import * as Yup from 'yup';
+import ValidationError from '../errors/ValidationError';
+import TextInput from '../../app/common/form/TextInput';
+
+export default observer(function RegsiterForm() {
+ const { userStore } = useStore();
+ return (
+
+ userStore.register(values).catch((error) => setErrors({ error }))
+ }
+ validationSchema={Yup.object({
+ displayName: Yup.string().required(),
+ username: Yup.string().required(),
+ email: Yup.string().required(),
+ password: Yup.string().required(),
+ })}
+ >
+ {({ handleSubmit, isSubmitting, errors, isValid, dirty }) => (
+
+ )}
+
+ );
+});