From e9e34c5fec8b15912a0d1f7c92baa4bccb12a146 Mon Sep 17 00:00:00 2001 From: Serghei Cebotari Date: Wed, 7 Feb 2024 16:32:34 +0300 Subject: [PATCH] Implement JWT tokens --- Database/Dockerfile | 4 +- Database/init-identity-database.sql | 97 ++++++ .../RhSolutions.Api.Tests.csproj | 2 +- RhSolutions.Api/ConnectionStringsUtil.cs | 30 ++ .../Controllers/AccountController.cs | 72 +++++ .../Controllers/ProductsController.cs | 4 +- .../Identity/20240206125053_Init.Designer.cs | 277 ++++++++++++++++++ .../Identity/20240206125053_Init.cs | 223 ++++++++++++++ .../Identity/IdentityContextModelSnapshot.cs | 274 +++++++++++++++++ RhSolutions.Api/Models/IdentityContext.cs | 9 + RhSolutions.Api/Models/IdentitySeedData.cs | 39 +++ RhSolutions.Api/Program.cs | 58 +++- RhSolutions.Api/RhSolutions.Api.csproj | 13 +- docker-compose.yml | 1 + 14 files changed, 1083 insertions(+), 20 deletions(-) create mode 100644 Database/init-identity-database.sql create mode 100644 RhSolutions.Api/ConnectionStringsUtil.cs create mode 100644 RhSolutions.Api/Controllers/AccountController.cs create mode 100644 RhSolutions.Api/Migrations/Identity/20240206125053_Init.Designer.cs create mode 100644 RhSolutions.Api/Migrations/Identity/20240206125053_Init.cs create mode 100644 RhSolutions.Api/Migrations/Identity/IdentityContextModelSnapshot.cs create mode 100644 RhSolutions.Api/Models/IdentityContext.cs create mode 100644 RhSolutions.Api/Models/IdentitySeedData.cs diff --git a/Database/Dockerfile b/Database/Dockerfile index a65c2ef..fea6d9d 100644 --- a/Database/Dockerfile +++ b/Database/Dockerfile @@ -1,6 +1,6 @@ FROM postgres:16.1-alpine AS build -ADD ./init-database.sql /docker-entrypoint-initdb.d -RUN chmod 644 /docker-entrypoint-initdb.d/init-database.sql +ADD ./*.sql /docker-entrypoint-initdb.d +RUN chmod 644 /docker-entrypoint-initdb.d/*.sql EXPOSE 5432 ENTRYPOINT [ "docker-entrypoint.sh" ] CMD [ "postgres" ] \ No newline at end of file diff --git a/Database/init-identity-database.sql b/Database/init-identity-database.sql new file mode 100644 index 0000000..9c4daa6 --- /dev/null +++ b/Database/init-identity-database.sql @@ -0,0 +1,97 @@ +CREATE TABLE IF NOT EXISTS "__EFMigrationsHistory" ( + "MigrationId" character varying(150) NOT NULL, + "ProductVersion" character varying(32) NOT NULL, + CONSTRAINT "PK___EFMigrationsHistory" PRIMARY KEY ("MigrationId") +); + +START TRANSACTION; + +CREATE TABLE "AspNetRoles" ( + "Id" text NOT NULL, + "Name" character varying(256), + "NormalizedName" character varying(256), + "ConcurrencyStamp" text, + CONSTRAINT "PK_AspNetRoles" PRIMARY KEY ("Id") +); + +CREATE TABLE "AspNetUsers" ( + "Id" text NOT NULL, + "UserName" character varying(256), + "NormalizedUserName" character varying(256), + "Email" character varying(256), + "NormalizedEmail" character varying(256), + "EmailConfirmed" boolean NOT NULL, + "PasswordHash" text, + "SecurityStamp" text, + "ConcurrencyStamp" text, + "PhoneNumber" text, + "PhoneNumberConfirmed" boolean NOT NULL, + "TwoFactorEnabled" boolean NOT NULL, + "LockoutEnd" timestamp with time zone, + "LockoutEnabled" boolean NOT NULL, + "AccessFailedCount" integer NOT NULL, + CONSTRAINT "PK_AspNetUsers" PRIMARY KEY ("Id") +); + +CREATE TABLE "AspNetRoleClaims" ( + "Id" integer GENERATED BY DEFAULT AS IDENTITY, + "RoleId" text NOT NULL, + "ClaimType" text, + "ClaimValue" text, + CONSTRAINT "PK_AspNetRoleClaims" PRIMARY KEY ("Id"), + CONSTRAINT "FK_AspNetRoleClaims_AspNetRoles_RoleId" FOREIGN KEY ("RoleId") REFERENCES "AspNetRoles" ("Id") ON DELETE CASCADE +); + +CREATE TABLE "AspNetUserClaims" ( + "Id" integer GENERATED BY DEFAULT AS IDENTITY, + "UserId" text NOT NULL, + "ClaimType" text, + "ClaimValue" text, + CONSTRAINT "PK_AspNetUserClaims" PRIMARY KEY ("Id"), + CONSTRAINT "FK_AspNetUserClaims_AspNetUsers_UserId" FOREIGN KEY ("UserId") REFERENCES "AspNetUsers" ("Id") ON DELETE CASCADE +); + +CREATE TABLE "AspNetUserLogins" ( + "LoginProvider" text NOT NULL, + "ProviderKey" text NOT NULL, + "ProviderDisplayName" text, + "UserId" text NOT NULL, + CONSTRAINT "PK_AspNetUserLogins" PRIMARY KEY ("LoginProvider", "ProviderKey"), + CONSTRAINT "FK_AspNetUserLogins_AspNetUsers_UserId" FOREIGN KEY ("UserId") REFERENCES "AspNetUsers" ("Id") ON DELETE CASCADE +); + +CREATE TABLE "AspNetUserRoles" ( + "UserId" text NOT NULL, + "RoleId" text NOT NULL, + CONSTRAINT "PK_AspNetUserRoles" PRIMARY KEY ("UserId", "RoleId"), + CONSTRAINT "FK_AspNetUserRoles_AspNetRoles_RoleId" FOREIGN KEY ("RoleId") REFERENCES "AspNetRoles" ("Id") ON DELETE CASCADE, + CONSTRAINT "FK_AspNetUserRoles_AspNetUsers_UserId" FOREIGN KEY ("UserId") REFERENCES "AspNetUsers" ("Id") ON DELETE CASCADE +); + +CREATE TABLE "AspNetUserTokens" ( + "UserId" text NOT NULL, + "LoginProvider" text NOT NULL, + "Name" text NOT NULL, + "Value" text, + CONSTRAINT "PK_AspNetUserTokens" PRIMARY KEY ("UserId", "LoginProvider", "Name"), + CONSTRAINT "FK_AspNetUserTokens_AspNetUsers_UserId" FOREIGN KEY ("UserId") REFERENCES "AspNetUsers" ("Id") ON DELETE CASCADE +); + +CREATE INDEX "IX_AspNetRoleClaims_RoleId" ON "AspNetRoleClaims" ("RoleId"); + +CREATE UNIQUE INDEX "RoleNameIndex" ON "AspNetRoles" ("NormalizedName"); + +CREATE INDEX "IX_AspNetUserClaims_UserId" ON "AspNetUserClaims" ("UserId"); + +CREATE INDEX "IX_AspNetUserLogins_UserId" ON "AspNetUserLogins" ("UserId"); + +CREATE INDEX "IX_AspNetUserRoles_RoleId" ON "AspNetUserRoles" ("RoleId"); + +CREATE INDEX "EmailIndex" ON "AspNetUsers" ("NormalizedEmail"); + +CREATE UNIQUE INDEX "UserNameIndex" ON "AspNetUsers" ("NormalizedUserName"); + +INSERT INTO "__EFMigrationsHistory" ("MigrationId", "ProductVersion") +VALUES ('20240206125053_Init', '8.0.1'); + +COMMIT; \ No newline at end of file diff --git a/RhSolutions.Api.Tests/RhSolutions.Api.Tests.csproj b/RhSolutions.Api.Tests/RhSolutions.Api.Tests.csproj index 5534bfe..cc4ff74 100644 --- a/RhSolutions.Api.Tests/RhSolutions.Api.Tests.csproj +++ b/RhSolutions.Api.Tests/RhSolutions.Api.Tests.csproj @@ -12,7 +12,7 @@ - + runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/RhSolutions.Api/ConnectionStringsUtil.cs b/RhSolutions.Api/ConnectionStringsUtil.cs new file mode 100644 index 0000000..b27eb4c --- /dev/null +++ b/RhSolutions.Api/ConnectionStringsUtil.cs @@ -0,0 +1,30 @@ +public class ConnectionStringsUtil +{ + private string dbHost; + private string dbPort; + private string dbName; + private string dbUser; + private string dbPassword; + private IConfiguration _configuration; + + public ConnectionStringsUtil(IConfiguration configuration) + { + _configuration = configuration; + dbHost = configuration["DB_HOST"] ?? "localhost"; + dbPort = configuration["DB_PORT"] ?? "5000"; + dbName = configuration["DB_DATABASE"] ?? "rhsolutions"; + dbUser = configuration["DB_USER"] ?? "chebser"; + dbPassword = configuration["DB_PASSWORD"] ?? "Rehau-987"; + } + public string GetRhDbString() + { + return _configuration["ConnectionsStrings:RhSolutionsLocal"] + ?? $"Host={dbHost};Port={dbPort};Database={dbName};Username={dbUser};Password={dbPassword}"; + } + + public string GetIdentityDbString() + { + return _configuration["ConnectionsStrings:RhSolutionsLocal"] + ?? $"Host={dbHost};Port={dbPort};Database=Identity;Username={dbUser};Password={dbPassword}"; + } +} diff --git a/RhSolutions.Api/Controllers/AccountController.cs b/RhSolutions.Api/Controllers/AccountController.cs new file mode 100644 index 0000000..647a6bc --- /dev/null +++ b/RhSolutions.Api/Controllers/AccountController.cs @@ -0,0 +1,72 @@ +using System.IdentityModel.Tokens.Jwt; +using System.Text; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Mvc; +using Microsoft.IdentityModel.Tokens; +using System.Security.Claims; + +namespace RhSolutions.Api.Controllers; + +[ApiController] +[Route("/api/account")] +public class AccountController : ControllerBase +{ + private SignInManager _signInManager; + private UserManager _userManager; + private IConfiguration _configuration; + public AccountController(SignInManager signInManager, + UserManager userManager, + IConfiguration configuration) + { + _signInManager = signInManager; + _userManager = userManager; + _configuration = configuration; + } + + /// + /// Получение токена + /// + /// + /// + [HttpPost("token")] + public async Task Token([FromBody] Credentials credentials) + { + if (await CheckPassword(credentials)) + { + JwtSecurityTokenHandler handler = new(); + string jwtSecret = _configuration["JWT_SECRET"] ?? "mold-smartness-arrive-overstate-aspirin"; + byte[] secret = Encoding.ASCII.GetBytes(jwtSecret); + SecurityTokenDescriptor descriptor = new() + { + Subject = new ClaimsIdentity(new Claim[] + { + new (ClaimTypes.Name, credentials.Username) + }), + Expires = DateTime.UtcNow.AddDays(1), + SigningCredentials = new(new SymmetricSecurityKey(secret), SecurityAlgorithms.HmacSha256Signature) + }; + SecurityToken token = handler.CreateToken(descriptor); + return Ok(new + { + Success = true, + Token = handler.WriteToken(token) + }); + } + return Unauthorized(); + } + + private async Task CheckPassword(Credentials credentials) + { + IdentityUser? user = await _userManager.FindByNameAsync(credentials.Username); + if (user != null) + { + return (await _signInManager.CheckPasswordSignInAsync(user, credentials.Password, true)).Succeeded; + } + return false; + } + public class Credentials + { + public string Username { get; set; } = string.Empty; + public string Password { get; set; } = string.Empty; + } +} \ No newline at end of file diff --git a/RhSolutions.Api/Controllers/ProductsController.cs b/RhSolutions.Api/Controllers/ProductsController.cs index 99c0af0..b2aa43e 100644 --- a/RhSolutions.Api/Controllers/ProductsController.cs +++ b/RhSolutions.Api/Controllers/ProductsController.cs @@ -1,7 +1,7 @@ using Microsoft.AspNetCore.Mvc; using RhSolutions.Models; using RhSolutions.Api.Services; -using System.Linq; +using Microsoft.AspNetCore.Authorization; namespace RhSolutions.Api.Controllers { @@ -45,6 +45,7 @@ namespace RhSolutions.Api.Controllers /// /// [HttpPost] + [Authorize(AuthenticationSchemes = "Identity.Application, Bearer")] public IActionResult PostProductsFromXls() { try @@ -80,6 +81,7 @@ namespace RhSolutions.Api.Controllers /// /// [HttpDelete] + [Authorize(AuthenticationSchemes = "Identity.Application, Bearer")] public IActionResult DeleteAllProducts() { List deleted = new(); diff --git a/RhSolutions.Api/Migrations/Identity/20240206125053_Init.Designer.cs b/RhSolutions.Api/Migrations/Identity/20240206125053_Init.Designer.cs new file mode 100644 index 0000000..9e2e3e8 --- /dev/null +++ b/RhSolutions.Api/Migrations/Identity/20240206125053_Init.Designer.cs @@ -0,0 +1,277 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using RhSolutions.Models; + +#nullable disable + +namespace RhSolutions.Api.Migrations.Identity +{ + [DbContext(typeof(IdentityContext))] + [Migration("20240206125053_Init")] + partial class Init + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.1") + .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("NormalizedEmail") + .HasDatabaseName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex"); + + b.ToTable("AspNetUsers", (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/RhSolutions.Api/Migrations/Identity/20240206125053_Init.cs b/RhSolutions.Api/Migrations/Identity/20240206125053_Init.cs new file mode 100644 index 0000000..c4f1052 --- /dev/null +++ b/RhSolutions.Api/Migrations/Identity/20240206125053_Init.cs @@ -0,0 +1,223 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace RhSolutions.Api.Migrations.Identity +{ + /// + public partial class Init : 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: "AspNetUsers", + 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_AspNetUsers", 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_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: "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/RhSolutions.Api/Migrations/Identity/IdentityContextModelSnapshot.cs b/RhSolutions.Api/Migrations/Identity/IdentityContextModelSnapshot.cs new file mode 100644 index 0000000..b20f179 --- /dev/null +++ b/RhSolutions.Api/Migrations/Identity/IdentityContextModelSnapshot.cs @@ -0,0 +1,274 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using RhSolutions.Models; + +#nullable disable + +namespace RhSolutions.Api.Migrations.Identity +{ + [DbContext(typeof(IdentityContext))] + partial class IdentityContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.1") + .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("NormalizedEmail") + .HasDatabaseName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex"); + + b.ToTable("AspNetUsers", (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/RhSolutions.Api/Models/IdentityContext.cs b/RhSolutions.Api/Models/IdentityContext.cs new file mode 100644 index 0000000..fbac8c9 --- /dev/null +++ b/RhSolutions.Api/Models/IdentityContext.cs @@ -0,0 +1,9 @@ +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Identity.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore; + +namespace RhSolutions.Models; +public class IdentityContext : IdentityDbContext +{ + public IdentityContext(DbContextOptions options) : base(options) { } +} \ No newline at end of file diff --git a/RhSolutions.Api/Models/IdentitySeedData.cs b/RhSolutions.Api/Models/IdentitySeedData.cs new file mode 100644 index 0000000..7cd2ff8 --- /dev/null +++ b/RhSolutions.Api/Models/IdentitySeedData.cs @@ -0,0 +1,39 @@ +using Microsoft.AspNetCore.Identity; + +namespace RhSolutions.Models; + +public class IdentitySeedData +{ + public static void CreateAdminAccount(IServiceProvider serviceProvider, IConfiguration configuration) + { + CreateAdminAccountAsync(serviceProvider, configuration).Wait(); + } + + public static async Task CreateAdminAccountAsync(IServiceProvider serviceProvider, IConfiguration configuration) + { + serviceProvider = serviceProvider.CreateScope().ServiceProvider; + UserManager userManager = serviceProvider.GetRequiredService>(); + RoleManager roleManager = serviceProvider.GetRequiredService>(); + + string username = "admin"; + string password = configuration["ADMIN_PASSWORD"] ?? "RhSolutionsPassword123!"; + string role = "Admins"; + + if (await userManager.FindByNameAsync(username) == null) + { + if (await roleManager.FindByNameAsync(role) == null) + { + await roleManager.CreateAsync(new(role)); + } + IdentityUser user = new() + { + UserName = username + }; + IdentityResult result = await userManager.CreateAsync(user, password); + if (result.Succeeded) + { + await userManager.AddToRoleAsync(user, role); + } + } + } +} \ No newline at end of file diff --git a/RhSolutions.Api/Program.cs b/RhSolutions.Api/Program.cs index 11f6722..ac5269c 100644 --- a/RhSolutions.Api/Program.cs +++ b/RhSolutions.Api/Program.cs @@ -5,26 +5,31 @@ using RhSolutions.Api.Middleware; using RhSolutions.MLModifiers; using Microsoft.OpenApi.Models; using System.Reflection; +using Microsoft.AspNetCore.Identity; +using System.Text; +using Microsoft.IdentityModel.Tokens; +using Microsoft.AspNetCore.Authentication.JwtBearer; +using System.Security.Claims; var builder = WebApplication.CreateBuilder(args); - -string dbHost = builder.Configuration["DB_HOST"] ?? "localhost", - dbPort = builder.Configuration["DB_PORT"] ?? "5000", - dbName = builder.Configuration["DB_DATABASE"] ?? "rhsolutions", - dbUser = builder.Configuration["DB_USER"] ?? "chebser", - dbPassword = builder.Configuration["DB_PASSWORD"] ?? "Rehau-987"; - -string connectionString = builder.Configuration["ConnectionsStrings:RhSolutionsLocal"] - ?? $"Host={dbHost};Port={dbPort};Database={dbName};Username={dbUser};Password={dbPassword}"; +ConnectionStringsUtil connectionStringsUtil = new(builder.Configuration); builder.Services.AddDbContext(opts => { - opts.UseNpgsql(connectionString); + opts.UseNpgsql(connectionStringsUtil.GetRhDbString()); if (builder.Environment.IsDevelopment()) { opts.EnableSensitiveDataLogging(true); } }); +builder.Services.AddDbContext(opts => +{ + opts.UseNpgsql(connectionStringsUtil.GetIdentityDbString()); +}); + +builder.Services.AddIdentity() + .AddEntityFrameworkStores(); + builder.Services.AddScoped() .AddScoped(); builder.Services.AddModifiers(); @@ -43,10 +48,38 @@ builder.Services.AddSwaggerGen(options => Email = @"serghei@cebotari.ru" } - }); + }); + var xmlFilename = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml"; options.IncludeXmlComments(Path.Combine(AppContext.BaseDirectory, xmlFilename)); }); +builder.Services.AddAuthentication() + .AddJwtBearer(opts => + { + opts.RequireHttpsMetadata = false; + opts.SaveToken = true; + opts.TokenValidationParameters = new() + { + ValidateIssuerSigningKey = true, + IssuerSigningKey = new SymmetricSecurityKey( + Encoding.ASCII.GetBytes(builder.Configuration["JWT_SECRET"] ?? "mold-smartness-arrive-overstate-aspirin")), + ValidateAudience = false, + ValidateIssuer = false + }; + opts.Events = new JwtBearerEvents() + { + OnTokenValidated = async context => + { + var userManager = context.HttpContext.RequestServices + .GetRequiredService>(); + var signInManager = context.HttpContext.RequestServices + .GetRequiredService>(); + string username = context.Principal!.FindFirst(ClaimTypes.Name)!.Value; + IdentityUser? idUser = await userManager.FindByNameAsync(username); + context.Principal = await signInManager.CreateUserPrincipalAsync(idUser!); + } + }; + }); var app = builder.Build(); @@ -57,5 +90,8 @@ app.UseSwagger().UseSwaggerUI(options => options.SwaggerEndpoint("/swagger/v1/swagger.json", "v1"); options.RoutePrefix = string.Empty; }); +app.UseAuthentication(); +app.UseAuthorization(); +IdentitySeedData.CreateAdminAccount(app.Services, app.Configuration); app.Run(); diff --git a/RhSolutions.Api/RhSolutions.Api.csproj b/RhSolutions.Api/RhSolutions.Api.csproj index eb3916a..7690469 100644 --- a/RhSolutions.Api/RhSolutions.Api.csproj +++ b/RhSolutions.Api/RhSolutions.Api.csproj @@ -10,18 +10,21 @@ - - - - + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive all - + + diff --git a/docker-compose.yml b/docker-compose.yml index 1c01b58..2e19f56 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -11,6 +11,7 @@ services: - DB_DATABASE=rhsolutions - DB_USER=chebser - DB_PASSWORD=Rehau-987 + - ASPNETCORE_ENVIRONMENT=Development networks: - rhsolutions volumes: