diff --git a/SessionZeroBackend/ApplicationDbContext.cs b/SessionZeroBackend/ApplicationDbContext.cs index 7bea0fa..f6c41b6 100644 --- a/SessionZeroBackend/ApplicationDbContext.cs +++ b/SessionZeroBackend/ApplicationDbContext.cs @@ -1,24 +1,23 @@ +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Identity.EntityFrameworkCore; using Microsoft.EntityFrameworkCore; using SessionZeroBackend.Models; namespace SessionZeroBackend; -public class ApplicationDbContext : DbContext +public class ApplicationDbContext : IdentityDbContext { public ApplicationDbContext(DbContextOptions options) : base(options) {} - - public DbSet Users { get; set; } protected override void OnModelCreating(ModelBuilder modelBuilder) { base.OnModelCreating(modelBuilder); - modelBuilder.Entity() - .HasIndex(u => u.Email) - .IsUnique(); - - modelBuilder.Entity() - .HasIndex(u => u.Username) - .IsUnique(); + modelBuilder.Entity(entity => + { + entity.ToTable("Users"); + entity.HasIndex(u => u.Email).IsUnique(); + entity.HasIndex(u => u.UserName).IsUnique(); + }); } } \ No newline at end of file diff --git a/SessionZeroBackend/Controllers/AuthController.cs b/SessionZeroBackend/Controllers/AuthController.cs new file mode 100644 index 0000000..f1ca03b --- /dev/null +++ b/SessionZeroBackend/Controllers/AuthController.cs @@ -0,0 +1,75 @@ +using System.IdentityModel.Tokens.Jwt; +using System.Security.Claims; +using System.Text; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Mvc; +using Microsoft.IdentityModel.Tokens; +using SessionZeroBackend.Models; + +namespace SessionZeroBackend.Controllers; + +[ApiController] +[Route("api/[controller]")] +public class AuthController : ControllerBase +{ + private readonly UserManager _userManager; + private readonly SignInManager _signInManager; + private readonly IConfiguration _configuration; + + public AuthController(UserManager userManager, SignInManager signInManager, IConfiguration configuration) + { + _userManager = userManager; + _signInManager = signInManager; + _configuration = configuration; + } + + [HttpPost("register")] + public async Task Register([FromBody] RegisterModel model) + { + var user = new IdentityUser { UserName = model.Username, Email = model.Email }; + var result = await _userManager.CreateAsync(user, model.Password); + + if (result.Succeeded) + { + return Ok(new { Message = "User registered successfully" }); + } + + return BadRequest(result.Errors); + } + + [HttpPost("login")] + public async Task Login([FromBody] LoginModel model) + { + var result = await _signInManager.PasswordSignInAsync(model.Username, model.Password, false, false); + + if (result.Succeeded) + { + var user = await _userManager.FindByNameAsync(model.Username); + var token = GenerateJwtToken(user); + return Ok(new { Token = token }); + } + + return Unauthorized(); + } + + private string GenerateJwtToken(IdentityUser user) + { + var claims = new[] + { + new Claim(JwtRegisteredClaimNames.Sub, user.UserName), + new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()) + }; + + var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_configuration["Jwt:Key"])); + var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256); + + var token = new JwtSecurityToken( + issuer: _configuration["Jwt:Issuer"], + audience: _configuration["Jwt:Audience"], + claims: claims, + expires: DateTime.Now.AddMinutes(30), + signingCredentials: creds); + + return new JwtSecurityTokenHandler().WriteToken(token); + } +} \ No newline at end of file diff --git a/SessionZeroBackend/Migrations/20250328022356_InitialCreate.Designer.cs b/SessionZeroBackend/Migrations/20250328022356_InitialCreate.Designer.cs deleted file mode 100644 index 5670a33..0000000 --- a/SessionZeroBackend/Migrations/20250328022356_InitialCreate.Designer.cs +++ /dev/null @@ -1,72 +0,0 @@ -// -using System; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; -using SessionZeroBackend; - -#nullable disable - -namespace SessionZeroBackend.Migrations -{ - [DbContext(typeof(ApplicationDbContext))] - [Migration("20250328022356_InitialCreate")] - partial class InitialCreate - { - /// - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder - .HasAnnotation("ProductVersion", "9.0.3") - .HasAnnotation("Relational:MaxIdentifierLength", 63); - - NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); - - modelBuilder.Entity("SessionZeroBackend.Models.User", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("Email") - .IsRequired() - .HasMaxLength(100) - .HasColumnType("character varying(100)"); - - b.Property("IsActive") - .HasColumnType("boolean"); - - b.Property("LastLoginAt") - .HasColumnType("timestamp with time zone"); - - b.Property("PasswordHash") - .IsRequired() - .HasColumnType("text"); - - b.Property("Username") - .IsRequired() - .HasMaxLength(100) - .HasColumnType("character varying(100)"); - - b.HasKey("Id"); - - b.HasIndex("Email") - .IsUnique(); - - b.HasIndex("Username") - .IsUnique(); - - b.ToTable("Users"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/SessionZeroBackend/Migrations/20250328022356_InitialCreate.cs b/SessionZeroBackend/Migrations/20250328022356_InitialCreate.cs deleted file mode 100644 index 99874f2..0000000 --- a/SessionZeroBackend/Migrations/20250328022356_InitialCreate.cs +++ /dev/null @@ -1,53 +0,0 @@ -using System; -using Microsoft.EntityFrameworkCore.Migrations; -using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; - -#nullable disable - -namespace SessionZeroBackend.Migrations -{ - /// - public partial class InitialCreate : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.CreateTable( - name: "Users", - columns: table => new - { - Id = table.Column(type: "integer", nullable: false) - .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), - Email = table.Column(type: "character varying(100)", maxLength: 100, nullable: false), - Username = table.Column(type: "character varying(100)", maxLength: 100, nullable: false), - PasswordHash = table.Column(type: "text", nullable: false), - CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), - LastLoginAt = table.Column(type: "timestamp with time zone", nullable: false), - IsActive = table.Column(type: "boolean", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_Users", x => x.Id); - }); - - migrationBuilder.CreateIndex( - name: "IX_Users_Email", - table: "Users", - column: "Email", - unique: true); - - migrationBuilder.CreateIndex( - name: "IX_Users_Username", - table: "Users", - column: "Username", - unique: true); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropTable( - name: "Users"); - } - } -} diff --git a/SessionZeroBackend/Migrations/20250328040633_InitialCreate.Designer.cs b/SessionZeroBackend/Migrations/20250328040633_InitialCreate.Designer.cs new file mode 100644 index 0000000..f00e21e --- /dev/null +++ b/SessionZeroBackend/Migrations/20250328040633_InitialCreate.Designer.cs @@ -0,0 +1,283 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using SessionZeroBackend; + +#nullable disable + +namespace SessionZeroBackend.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + [Migration("20250328040633_InitialCreate")] + partial class InitialCreate + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.3") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("text"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("NormalizedName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + 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"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + 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.IdentityUser", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("AccessFailedCount") + .HasColumnType("integer"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("text"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("EmailConfirmed") + .HasColumnType("boolean"); + + b.Property("LockoutEnabled") + .HasColumnType("boolean"); + + b.Property("LockoutEnd") + .HasColumnType("timestamp with time zone"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("PasswordHash") + .HasColumnType("text"); + + b.Property("PhoneNumber") + .HasColumnType("text"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("boolean"); + + b.Property("SecurityStamp") + .HasColumnType("text"); + + b.Property("TwoFactorEnabled") + .HasColumnType("boolean"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.HasKey("Id"); + + b.HasIndex("Email") + .IsUnique(); + + b.HasIndex("NormalizedEmail") + .HasDatabaseName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex"); + + b.HasIndex("UserName") + .IsUnique(); + + b.ToTable("Users", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + 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("Microsoft.AspNetCore.Identity.IdentityUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", 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("Microsoft.AspNetCore.Identity.IdentityUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/SessionZeroBackend/Migrations/20250328040633_InitialCreate.cs b/SessionZeroBackend/Migrations/20250328040633_InitialCreate.cs new file mode 100644 index 0000000..f26ff5b --- /dev/null +++ b/SessionZeroBackend/Migrations/20250328040633_InitialCreate.cs @@ -0,0 +1,235 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace SessionZeroBackend.Migrations +{ + /// + public partial class InitialCreate : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "AspNetRoles", + columns: table => new + { + Id = table.Column(type: "text", nullable: false), + Name = table.Column(type: "character varying(256)", maxLength: 256, nullable: true), + NormalizedName = table.Column(type: "character varying(256)", maxLength: 256, nullable: true), + ConcurrencyStamp = table.Column(type: "text", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetRoles", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "Users", + columns: table => new + { + Id = table.Column(type: "text", nullable: false), + UserName = table.Column(type: "character varying(256)", maxLength: 256, nullable: true), + NormalizedUserName = table.Column(type: "character varying(256)", maxLength: 256, nullable: true), + Email = table.Column(type: "character varying(256)", maxLength: 256, nullable: true), + NormalizedEmail = table.Column(type: "character varying(256)", maxLength: 256, nullable: true), + EmailConfirmed = table.Column(type: "boolean", 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: "boolean", nullable: false), + TwoFactorEnabled = table.Column(type: "boolean", nullable: false), + LockoutEnd = table.Column(type: "timestamp with time zone", nullable: true), + LockoutEnabled = table.Column(type: "boolean", nullable: false), + AccessFailedCount = table.Column(type: "integer", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Users", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "AspNetRoleClaims", + columns: table => new + { + Id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + 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("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + 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_Users_UserId", + column: x => x.UserId, + principalTable: "Users", + 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_Users_UserId", + column: x => x.UserId, + principalTable: "Users", + 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_Users_UserId", + column: x => x.UserId, + principalTable: "Users", + 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_Users_UserId", + column: x => x.UserId, + principalTable: "Users", + 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: "Users", + column: "NormalizedEmail"); + + migrationBuilder.CreateIndex( + name: "IX_Users_Email", + table: "Users", + column: "Email", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_Users_UserName", + table: "Users", + column: "UserName", + unique: true); + + migrationBuilder.CreateIndex( + name: "UserNameIndex", + table: "Users", + column: "NormalizedUserName", + unique: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + 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: "Users"); + } + } +} diff --git a/SessionZeroBackend/Migrations/ApplicationDbContextModelSnapshot.cs b/SessionZeroBackend/Migrations/ApplicationDbContextModelSnapshot.cs index 4aa93b1..c7ff322 100644 --- a/SessionZeroBackend/Migrations/ApplicationDbContextModelSnapshot.cs +++ b/SessionZeroBackend/Migrations/ApplicationDbContextModelSnapshot.cs @@ -22,7 +22,33 @@ namespace SessionZeroBackend.Migrations NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); - modelBuilder.Entity("SessionZeroBackend.Models.User", b => + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("text"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("NormalizedName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + 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() @@ -30,38 +56,223 @@ namespace SessionZeroBackend.Migrations NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); + b.Property("ClaimType") + .HasColumnType("text"); - b.Property("Email") - .IsRequired() - .HasMaxLength(100) - .HasColumnType("character varying(100)"); + b.Property("ClaimValue") + .HasColumnType("text"); - b.Property("IsActive") - .HasColumnType("boolean"); - - b.Property("LastLoginAt") - .HasColumnType("timestamp with time zone"); - - b.Property("PasswordHash") + b.Property("RoleId") .IsRequired() .HasColumnType("text"); - b.Property("Username") - .IsRequired() - .HasMaxLength(100) - .HasColumnType("character varying(100)"); + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUser", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("AccessFailedCount") + .HasColumnType("integer"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("text"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("EmailConfirmed") + .HasColumnType("boolean"); + + b.Property("LockoutEnabled") + .HasColumnType("boolean"); + + b.Property("LockoutEnd") + .HasColumnType("timestamp with time zone"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("PasswordHash") + .HasColumnType("text"); + + b.Property("PhoneNumber") + .HasColumnType("text"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("boolean"); + + b.Property("SecurityStamp") + .HasColumnType("text"); + + b.Property("TwoFactorEnabled") + .HasColumnType("boolean"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); b.HasKey("Id"); b.HasIndex("Email") .IsUnique(); - b.HasIndex("Username") + b.HasIndex("NormalizedEmail") + .HasDatabaseName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex"); + + b.HasIndex("UserName") .IsUnique(); - b.ToTable("Users"); + b.ToTable("Users", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + 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("Microsoft.AspNetCore.Identity.IdentityUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", 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("Microsoft.AspNetCore.Identity.IdentityUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); }); #pragma warning restore 612, 618 } diff --git a/SessionZeroBackend/Models/LoginModel.cs b/SessionZeroBackend/Models/LoginModel.cs new file mode 100644 index 0000000..cddefe8 --- /dev/null +++ b/SessionZeroBackend/Models/LoginModel.cs @@ -0,0 +1,7 @@ +namespace SessionZeroBackend.Models; + +public class LoginModel +{ + public string Username { get; set; } + public string Password { get; set; } +} \ No newline at end of file diff --git a/SessionZeroBackend/Models/RegisterModel.cs b/SessionZeroBackend/Models/RegisterModel.cs new file mode 100644 index 0000000..f924947 --- /dev/null +++ b/SessionZeroBackend/Models/RegisterModel.cs @@ -0,0 +1,8 @@ +namespace SessionZeroBackend.Models; + +public class RegisterModel +{ + public string Username { get; set; } + public string Email { get; set; } + public string Password { get; set; } +} \ No newline at end of file diff --git a/SessionZeroBackend/Models/User.cs b/SessionZeroBackend/Models/User.cs index b2bbaa0..027071d 100644 --- a/SessionZeroBackend/Models/User.cs +++ b/SessionZeroBackend/Models/User.cs @@ -1,28 +1,39 @@ using System.ComponentModel.DataAnnotations; -using System.ComponentModel.DataAnnotations.Schema; - -namespace SessionZeroBackend.Models; - -public class User -{ - [Key] - [DatabaseGenerated(DatabaseGeneratedOption.Identity)] - public int Id { get; set; } + using System.ComponentModel.DataAnnotations.Schema; + using Microsoft.AspNetCore.Identity; - [Required] - [EmailAddress] - [MaxLength(100)] - public string Email { get; set; } + namespace SessionZeroBackend.Models; - [Required] - [MaxLength(100)] - public string Username { get; set; } + public class User : IdentityUser + { + [Key] + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public int Id { get; set; } + + [Required] + [EmailAddress] + [MaxLength(100)] + public override string Email { get; set; } + + [Required] + public override string PasswordHash { get; set; } + + public DateTime CreatedAt { get; set; } = DateTime.UtcNow; + public DateTime LastLoginAt { get; set; } + + public bool IsActive { get; set; } = true; - [Required] - public string PasswordHash { get; set; } - - public DateTime CreatedAt { get; set; } = DateTime.UtcNow; - public DateTime LastLoginAt { get; set; } - - public bool IsActive { get; set; } = true; -} \ No newline at end of file + // Additional properties from IdentityUser + public override int AccessFailedCount { get; set; } + public override string ConcurrencyStamp { get; set; } + public override bool EmailConfirmed { get; set; } + public override bool LockoutEnabled { get; set; } + public override DateTimeOffset? LockoutEnd { get; set; } + public override string NormalizedEmail { get; set; } + public override string NormalizedUserName { get; set; } + public override string PhoneNumber { get; set; } + public override bool PhoneNumberConfirmed { get; set; } + public override string SecurityStamp { get; set; } + public override bool TwoFactorEnabled { get; set; } + public override string UserName { get; set; } + } \ No newline at end of file diff --git a/SessionZeroBackend/Program.cs b/SessionZeroBackend/Program.cs index 6210936..7cf048f 100644 --- a/SessionZeroBackend/Program.cs +++ b/SessionZeroBackend/Program.cs @@ -1,5 +1,11 @@ +using System.Text; +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.AspNetCore.Identity; using Microsoft.EntityFrameworkCore; +using Microsoft.IdentityModel.Tokens; +using Microsoft.OpenApi.Models; using SessionZeroBackend; +using SessionZeroBackend.Models; var builder = WebApplication.CreateBuilder(args); @@ -9,10 +15,16 @@ var app = builder.Build(); if (app.Environment.IsDevelopment()) { - //TODO: Add Swagger Stuff + app.UseDeveloperExceptionPage(); + app.UseSwagger(); + app.UseSwaggerUI(c => + { + c.SwaggerEndpoint("/swagger/v1/swagger.json", "SessionZero API V1"); + }); } app.UseHttpsRedirection(); +app.UseAuthentication(); app.UseAuthorization(); app.MapControllers(); @@ -29,10 +41,36 @@ void ConfigureServices(WebApplicationBuilder builder) configuration.GetConnectionString("DefaultConnection") ?? throw new NullReferenceException("No connection string found.")); }); + + services.AddIdentity() + .AddEntityFrameworkStores() + .AddDefaultTokenProviders(); + + var key = Encoding.ASCII.GetBytes(configuration["Jwt:Key"]); + + services.AddAuthentication(options => + { + options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; + options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; + }) + .AddJwtBearer(options => + { + options.TokenValidationParameters = new TokenValidationParameters + { + ValidateIssuer = true, + ValidateAudience = true, + ValidateLifetime = true, + ValidateIssuerSigningKey = true, + ValidIssuer = configuration["Jwt:Issuer"], + ValidAudience = configuration["Jwt:Audience"], + IssuerSigningKey = new SymmetricSecurityKey(key) + }; + }); services.AddControllers(); - - // TODO: Swagger for API documentation - // services.AddEndpointsApiExplorer(); - // services.AddSwaggerGen(); + + services.AddSwaggerGen(c => + { + c.SwaggerDoc("v1", new OpenApiInfo {Title = "SessionZero API", Version = "v1"}); + }); } \ No newline at end of file diff --git a/SessionZeroBackend/SessionZeroBackend.csproj b/SessionZeroBackend/SessionZeroBackend.csproj index bb56b09..016bf84 100644 --- a/SessionZeroBackend/SessionZeroBackend.csproj +++ b/SessionZeroBackend/SessionZeroBackend.csproj @@ -8,6 +8,8 @@ + + @@ -18,6 +20,7 @@ + diff --git a/SessionZeroBackend/appsettings.json b/SessionZeroBackend/appsettings.json index 5283fbc..129142a 100644 --- a/SessionZeroBackend/appsettings.json +++ b/SessionZeroBackend/appsettings.json @@ -8,5 +8,10 @@ "Microsoft.AspNetCore": "Warning" } }, - "AllowedHosts": "*" + "AllowedHosts": "*", + "Jwt": { + "Key": "your-256-bit-secret-key-hereasdgaerfhafebsaerhaerhaerhbaeraerh", + "Issuer": "YourIssuer", + "Audience": "YourAudience" + } } diff --git a/SessionZeroBackend/deleteme.txt b/SessionZeroBackend/deleteme.txt new file mode 100644 index 0000000..31c2e34 --- /dev/null +++ b/SessionZeroBackend/deleteme.txt @@ -0,0 +1,65 @@ +Npgsql.PostgresException (0x80004005): 42703: column u.AccessFailedCount does not exist + +POSITION: 16 + at Npgsql.Internal.NpgsqlConnector.ReadMessageLong(Boolean async, DataRowLoadingMode dataRowLoadingMode, Boolean readingNotifications, Boolean isReadingPrependedMessage) + at System.Runtime.CompilerServices.PoolingAsyncValueTaskMethodBuilder`1.StateMachineBox`1.System.Threading.Tasks.Sources.IValueTaskSource.GetResult(Int16 token) + at Npgsql.NpgsqlDataReader.NextResult(Boolean async, Boolean isConsuming, CancellationToken cancellationToken) + at Npgsql.NpgsqlDataReader.NextResult(Boolean async, Boolean isConsuming, CancellationToken cancellationToken) + at Npgsql.NpgsqlCommand.ExecuteReader(Boolean async, CommandBehavior behavior, CancellationToken cancellationToken) + at Npgsql.NpgsqlCommand.ExecuteReader(Boolean async, CommandBehavior behavior, CancellationToken cancellationToken) + at Npgsql.NpgsqlCommand.ExecuteDbDataReaderAsync(CommandBehavior behavior, CancellationToken cancellationToken) + at Microsoft.EntityFrameworkCore.Storage.RelationalCommand.ExecuteReaderAsync(RelationalCommandParameterObject parameterObject, CancellationToken cancellationToken) + at Microsoft.EntityFrameworkCore.Storage.RelationalCommand.ExecuteReaderAsync(RelationalCommandParameterObject parameterObject, CancellationToken cancellationToken) + at Microsoft.EntityFrameworkCore.Query.Internal.SingleQueryingEnumerable`1.AsyncEnumerator.InitializeReaderAsync(AsyncEnumerator enumerator, CancellationToken cancellationToken) + at Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal.NpgsqlExecutionStrategy.ExecuteAsync[TState,TResult](TState state, Func`4 operation, Func`4 verifySucceeded, CancellationToken cancellationToken) + at Microsoft.EntityFrameworkCore.Query.Internal.SingleQueryingEnumerable`1.AsyncEnumerator.MoveNextAsync() + at Microsoft.EntityFrameworkCore.Query.ShapedQueryCompilingExpressionVisitor.SingleOrDefaultAsync[TSource](IAsyncEnumerable`1 asyncEnumerable, CancellationToken cancellationToken) + at Microsoft.EntityFrameworkCore.Query.ShapedQueryCompilingExpressionVisitor.SingleOrDefaultAsync[TSource](IAsyncEnumerable`1 asyncEnumerable, CancellationToken cancellationToken) + at Microsoft.AspNetCore.Identity.UserManager`1.FindByNameAsync(String userName) + at Microsoft.AspNetCore.Identity.UserValidator`1.ValidateUserName(UserManager`1 manager, TUser user) + at Microsoft.AspNetCore.Identity.UserValidator`1.ValidateAsync(UserManager`1 manager, TUser user) + at Microsoft.AspNetCore.Identity.UserManager`1.ValidateUserAsync(TUser user) + at Microsoft.AspNetCore.Identity.UserManager`1.CreateAsync(TUser user) + at Microsoft.AspNetCore.Identity.UserManager`1.CreateAsync(TUser user, String password) + at SessionZeroBackend.Controllers.AuthController.Register(RegisterModel model) in /home/chris/mnt/data/Projects/SessionZero/SessionZeroBackend/Controllers/AuthController.cs:line 30 + at Microsoft.AspNetCore.Mvc.Infrastructure.ActionMethodExecutor.TaskOfIActionResultExecutor.Execute(ActionContext actionContext, IActionResultTypeMapper mapper, ObjectMethodExecutor executor, Object controller, Object[] arguments) + at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.g__Logged|12_1(ControllerActionInvoker invoker) + at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.g__Awaited|10_0(ControllerActionInvoker invoker, Task lastTask, State next, Scope scope, Object state, Boolean isCompleted) + at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.Rethrow(ActionExecutedContextSealed context) + at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.Next(State& next, Scope& scope, Object& state, Boolean& isCompleted) + at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.g__Awaited|13_0(ControllerActionInvoker invoker, Task lastTask, State next, Scope scope, Object state, Boolean isCompleted) + at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.g__Awaited|20_0(ResourceInvoker invoker, Task lastTask, State next, Scope scope, Object state, Boolean isCompleted) + at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.g__Logged|17_1(ResourceInvoker invoker) + at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.g__Logged|17_1(ResourceInvoker invoker) + at Microsoft.AspNetCore.Authorization.AuthorizationMiddleware.Invoke(HttpContext context) + at Microsoft.AspNetCore.Authentication.AuthenticationMiddleware.Invoke(HttpContext context) + at Swashbuckle.AspNetCore.SwaggerUI.SwaggerUIMiddleware.Invoke(HttpContext httpContext) + at Swashbuckle.AspNetCore.Swagger.SwaggerMiddleware.Invoke(HttpContext httpContext, ISwaggerProvider swaggerProvider) + at Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddlewareImpl.Invoke(HttpContext context) + Exception data: + Severity: ERROR + SqlState: 42703 + MessageText: column u.AccessFailedCount does not exist + Position: 16 + File: parse_relation.c + Line: 3722 + Routine: errorMissingColumn + +HEADERS +======= +Accept: */* +Connection: keep-alive +Host: localhost:5161 +User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:136.0) Gecko/20100101 Firefox/136.0 +Accept-Encoding: gzip, deflate, br, zstd +Accept-Language: en-US,en;q=0.5 +Content-Type: application/json +Cookie: Webstorm-2dae7c8a=d26fa736-2d49-435c-aceb-8b0e81d410bc +Origin: http://localhost:5161 +Referer: http://localhost:5161/swagger/index.html +Content-Length: 78 +DNT: 1 +Sec-Fetch-Dest: empty +Sec-Fetch-Mode: cors +Sec-Fetch-Site: same-origin +Priority: u=0