diff options
author | Serghei Cebotari <serghei@cebotari.ru> | 2024-02-07 16:32:34 +0300 |
---|---|---|
committer | Serghei Cebotari <serghei@cebotari.ru> | 2024-02-07 16:32:34 +0300 |
commit | e9e34c5fec8b15912a0d1f7c92baa4bccb12a146 (patch) | |
tree | f8ffdd29efb1bdfa2a21fd18f70da8cba2a6bf85 | |
parent | 3c6436256ea058fd746f634fd8c4a4e852dc77aa (diff) |
Implement JWT tokens
-rw-r--r-- | Database/Dockerfile | 4 | ||||
-rw-r--r-- | Database/init-identity-database.sql | 97 | ||||
-rw-r--r-- | RhSolutions.Api.Tests/RhSolutions.Api.Tests.csproj | 2 | ||||
-rw-r--r-- | RhSolutions.Api/ConnectionStringsUtil.cs | 30 | ||||
-rw-r--r-- | RhSolutions.Api/Controllers/AccountController.cs | 72 | ||||
-rw-r--r-- | RhSolutions.Api/Controllers/ProductsController.cs | 4 | ||||
-rw-r--r-- | RhSolutions.Api/Migrations/Identity/20240206125053_Init.Designer.cs | 277 | ||||
-rw-r--r-- | RhSolutions.Api/Migrations/Identity/20240206125053_Init.cs | 223 | ||||
-rw-r--r-- | RhSolutions.Api/Migrations/Identity/IdentityContextModelSnapshot.cs | 274 | ||||
-rw-r--r-- | RhSolutions.Api/Models/IdentityContext.cs | 9 | ||||
-rw-r--r-- | RhSolutions.Api/Models/IdentitySeedData.cs | 39 | ||||
-rw-r--r-- | RhSolutions.Api/Program.cs | 58 | ||||
-rw-r--r-- | RhSolutions.Api/RhSolutions.Api.csproj | 13 | ||||
-rw-r--r-- | docker-compose.yml | 1 |
14 files changed, 1083 insertions, 20 deletions
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 @@ <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.8.0" /> <PackageReference Include="NUnit" Version="4.0.1" /> <PackageReference Include="NUnit3TestAdapter" Version="4.5.0" /> - <PackageReference Include="NUnit.Analyzers" Version="3.10.0"> + <PackageReference Include="NUnit.Analyzers" Version="4.0.1"> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <PrivateAssets>all</PrivateAssets> </PackageReference> 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<IdentityUser> _signInManager; + private UserManager<IdentityUser> _userManager; + private IConfiguration _configuration; + public AccountController(SignInManager<IdentityUser> signInManager, + UserManager<IdentityUser> userManager, + IConfiguration configuration) + { + _signInManager = signInManager; + _userManager = userManager; + _configuration = configuration; + } + + /// <summary> + /// Получение токена + /// </summary> + /// <param name="credentials"></param> + /// <returns></returns> + [HttpPost("token")] + public async Task<IActionResult> 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<bool> 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 /// </summary> /// <returns></returns> [HttpPost] + [Authorize(AuthenticationSchemes = "Identity.Application, Bearer")] public IActionResult PostProductsFromXls() { try @@ -80,6 +81,7 @@ namespace RhSolutions.Api.Controllers /// </summary> /// <returns></returns> [HttpDelete] + [Authorize(AuthenticationSchemes = "Identity.Application, Bearer")] public IActionResult DeleteAllProducts() { List<Product> 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 @@ +// <auto-generated /> +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 + { + /// <inheritdoc /> + 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<string>("Id") + .HasColumnType("text"); + + b.Property<string>("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("text"); + + b.Property<string>("Name") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property<string>("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<string>", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id")); + + b.Property<string>("ClaimType") + .HasColumnType("text"); + + b.Property<string>("ClaimValue") + .HasColumnType("text"); + + b.Property<string>("RoleId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUser", b => + { + b.Property<string>("Id") + .HasColumnType("text"); + + b.Property<int>("AccessFailedCount") + .HasColumnType("integer"); + + b.Property<string>("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("text"); + + b.Property<string>("Email") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property<bool>("EmailConfirmed") + .HasColumnType("boolean"); + + b.Property<bool>("LockoutEnabled") + .HasColumnType("boolean"); + + b.Property<DateTimeOffset?>("LockoutEnd") + .HasColumnType("timestamp with time zone"); + + b.Property<string>("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property<string>("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property<string>("PasswordHash") + .HasColumnType("text"); + + b.Property<string>("PhoneNumber") + .HasColumnType("text"); + + b.Property<bool>("PhoneNumberConfirmed") + .HasColumnType("boolean"); + + b.Property<string>("SecurityStamp") + .HasColumnType("text"); + + b.Property<bool>("TwoFactorEnabled") + .HasColumnType("boolean"); + + b.Property<string>("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<string>", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id")); + + b.Property<string>("ClaimType") + .HasColumnType("text"); + + b.Property<string>("ClaimValue") + .HasColumnType("text"); + + b.Property<string>("UserId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b => + { + b.Property<string>("LoginProvider") + .HasColumnType("text"); + + b.Property<string>("ProviderKey") + .HasColumnType("text"); + + b.Property<string>("ProviderDisplayName") + .HasColumnType("text"); + + b.Property<string>("UserId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b => + { + b.Property<string>("UserId") + .HasColumnType("text"); + + b.Property<string>("RoleId") + .HasColumnType("text"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b => + { + b.Property<string>("UserId") + .HasColumnType("text"); + + b.Property<string>("LoginProvider") + .HasColumnType("text"); + + b.Property<string>("Name") + .HasColumnType("text"); + + b.Property<string>("Value") + .HasColumnType("text"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", 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<string>", 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 +{ + /// <inheritdoc /> + public partial class Init : Migration + { + /// <inheritdoc /> + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "AspNetRoles", + columns: table => new + { + Id = table.Column<string>(type: "text", nullable: false), + Name = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: true), + NormalizedName = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: true), + ConcurrencyStamp = table.Column<string>(type: "text", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetRoles", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "AspNetUsers", + columns: table => new + { + Id = table.Column<string>(type: "text", nullable: false), + UserName = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: true), + NormalizedUserName = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: true), + Email = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: true), + NormalizedEmail = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: true), + EmailConfirmed = table.Column<bool>(type: "boolean", nullable: false), + PasswordHash = table.Column<string>(type: "text", nullable: true), + SecurityStamp = table.Column<string>(type: "text", nullable: true), + ConcurrencyStamp = table.Column<string>(type: "text", nullable: true), + PhoneNumber = table.Column<string>(type: "text", nullable: true), + PhoneNumberConfirmed = table.Column<bool>(type: "boolean", nullable: false), + TwoFactorEnabled = table.Column<bool>(type: "boolean", nullable: false), + LockoutEnd = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: true), + LockoutEnabled = table.Column<bool>(type: "boolean", nullable: false), + AccessFailedCount = table.Column<int>(type: "integer", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetUsers", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "AspNetRoleClaims", + columns: table => new + { + Id = table.Column<int>(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + RoleId = table.Column<string>(type: "text", nullable: false), + ClaimType = table.Column<string>(type: "text", nullable: true), + ClaimValue = table.Column<string>(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<int>(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + UserId = table.Column<string>(type: "text", nullable: false), + ClaimType = table.Column<string>(type: "text", nullable: true), + ClaimValue = table.Column<string>(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<string>(type: "text", nullable: false), + ProviderKey = table.Column<string>(type: "text", nullable: false), + ProviderDisplayName = table.Column<string>(type: "text", nullable: true), + UserId = table.Column<string>(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<string>(type: "text", nullable: false), + RoleId = table.Column<string>(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<string>(type: "text", nullable: false), + LoginProvider = table.Column<string>(type: "text", nullable: false), + Name = table.Column<string>(type: "text", nullable: false), + Value = table.Column<string>(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); + } + + /// <inheritdoc /> + 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 @@ +// <auto-generated /> +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<string>("Id") + .HasColumnType("text"); + + b.Property<string>("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("text"); + + b.Property<string>("Name") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property<string>("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<string>", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id")); + + b.Property<string>("ClaimType") + .HasColumnType("text"); + + b.Property<string>("ClaimValue") + .HasColumnType("text"); + + b.Property<string>("RoleId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUser", b => + { + b.Property<string>("Id") + .HasColumnType("text"); + + b.Property<int>("AccessFailedCount") + .HasColumnType("integer"); + + b.Property<string>("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("text"); + + b.Property<string>("Email") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property<bool>("EmailConfirmed") + .HasColumnType("boolean"); + + b.Property<bool>("LockoutEnabled") + .HasColumnType("boolean"); + + b.Property<DateTimeOffset?>("LockoutEnd") + .HasColumnType("timestamp with time zone"); + + b.Property<string>("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property<string>("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property<string>("PasswordHash") + .HasColumnType("text"); + + b.Property<string>("PhoneNumber") + .HasColumnType("text"); + + b.Property<bool>("PhoneNumberConfirmed") + .HasColumnType("boolean"); + + b.Property<string>("SecurityStamp") + .HasColumnType("text"); + + b.Property<bool>("TwoFactorEnabled") + .HasColumnType("boolean"); + + b.Property<string>("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<string>", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id")); + + b.Property<string>("ClaimType") + .HasColumnType("text"); + + b.Property<string>("ClaimValue") + .HasColumnType("text"); + + b.Property<string>("UserId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b => + { + b.Property<string>("LoginProvider") + .HasColumnType("text"); + + b.Property<string>("ProviderKey") + .HasColumnType("text"); + + b.Property<string>("ProviderDisplayName") + .HasColumnType("text"); + + b.Property<string>("UserId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b => + { + b.Property<string>("UserId") + .HasColumnType("text"); + + b.Property<string>("RoleId") + .HasColumnType("text"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b => + { + b.Property<string>("UserId") + .HasColumnType("text"); + + b.Property<string>("LoginProvider") + .HasColumnType("text"); + + b.Property<string>("Name") + .HasColumnType("text"); + + b.Property<string>("Value") + .HasColumnType("text"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", 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<string>", 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<IdentityUser> +{ + public IdentityContext(DbContextOptions<IdentityContext> 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<IdentityUser> userManager = serviceProvider.GetRequiredService<UserManager<IdentityUser>>(); + RoleManager<IdentityRole> roleManager = serviceProvider.GetRequiredService<RoleManager<IdentityRole>>(); + + 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<RhSolutionsContext>(opts => { - opts.UseNpgsql(connectionString); + opts.UseNpgsql(connectionStringsUtil.GetRhDbString()); if (builder.Environment.IsDevelopment()) { opts.EnableSensitiveDataLogging(true); } }); +builder.Services.AddDbContext<IdentityContext>(opts => +{ + opts.UseNpgsql(connectionStringsUtil.GetIdentityDbString()); +}); + +builder.Services.AddIdentity<IdentityUser, IdentityRole>() + .AddEntityFrameworkStores<IdentityContext>(); + builder.Services.AddScoped<IPricelistParser, ClosedXMLParser>() .AddScoped<IProductTypePredicter, ProductTypePredicter>(); 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<UserManager<IdentityUser>>(); + var signInManager = context.HttpContext.RequestServices + .GetRequiredService<SignInManager<IdentityUser>>(); + 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 @@ </PropertyGroup> <ItemGroup> - <PackageReference Include="ClosedXML" Version="0.102.1" /> - <PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.0" /> - <PackageReference Include="Microsoft.EntityFrameworkCore.Abstractions" Version="8.0.0" /> - <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.0"> + <PackageReference Include="ClosedXML" Version="0.102.2" /> + <PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.1" /> + <PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="8.0.1" /> + <PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.1" /> + <PackageReference Include="Microsoft.EntityFrameworkCore.Abstractions" Version="8.0.1" /> + <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.1"> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <PrivateAssets>all</PrivateAssets> </PackageReference> - <PackageReference Include="Microsoft.ML" Version="3.0.0" /> + <PackageReference Include="Microsoft.ML" Version="3.0.1" /> <PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="8.0.0" /> <PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL.Design" Version="1.1.0" /> <PackageReference Include="Rhsolutions.ProductSku" Version="1.0.2" /> <PackageReference Include="Swashbuckle.AspNetCore" Version="6.5.0" /> + <PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="7.3.1" /> </ItemGroup> <ItemGroup> 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: |