From afb3ce8924ec0767019d9ada7b73108bd2fdc692 Mon Sep 17 00:00:00 2001 From: Vladymor Date: Mon, 17 Mar 2025 18:38:45 +0200 Subject: [PATCH] Added controllers login and register Added JWT Service for creating JWT-keys Added Admin Seeder in Database Added basic IdentityServer Configuration Added class Config with ApiScope, ApiRecources, Clients --- Api/Api.csproj | 20 +- Api/Config.cs | 41 +++ Api/Controllers/AccountController.cs | 96 +++++ Api/Controllers/WeatherForecastController.cs | 33 -- Api/DTOs/Account/LoginDto.cs | 12 + Api/DTOs/Account/RegisterDto.cs | 20 ++ Api/DTOs/Account/UserDto.cs | 9 + Api/Database/Context.cs | 21 ++ Api/Database/DataSeed.cs | 47 +++ ...16104624_AddingAdminToDatabase.Designer.cs | 338 ++++++++++++++++++ .../20250316104624_AddingAdminToDatabase.cs | 252 +++++++++++++ .../Migrations/ContextModelSnapshot.cs | 335 +++++++++++++++++ Api/Entities/User.cs | 15 + Api/Program.cs | 97 ++++- Api/Properties/launchSettings.json | 12 +- Api/Services/JWTService.cs | 50 +++ Api/WeatherForecast.cs | 13 - Api/appsettings.json | 63 +++- Api/tempkey.jwk | 1 + 19 files changed, 1412 insertions(+), 63 deletions(-) create mode 100644 Api/Config.cs create mode 100644 Api/Controllers/AccountController.cs delete mode 100644 Api/Controllers/WeatherForecastController.cs create mode 100644 Api/DTOs/Account/LoginDto.cs create mode 100644 Api/DTOs/Account/RegisterDto.cs create mode 100644 Api/DTOs/Account/UserDto.cs create mode 100644 Api/Database/Context.cs create mode 100644 Api/Database/DataSeed.cs create mode 100644 Api/Database/Migrations/20250316104624_AddingAdminToDatabase.Designer.cs create mode 100644 Api/Database/Migrations/20250316104624_AddingAdminToDatabase.cs create mode 100644 Api/Database/Migrations/ContextModelSnapshot.cs create mode 100644 Api/Entities/User.cs create mode 100644 Api/Services/JWTService.cs delete mode 100644 Api/WeatherForecast.cs create mode 100644 Api/tempkey.jwk diff --git a/Api/Api.csproj b/Api/Api.csproj index 4a73db9..dd5111d 100644 --- a/Api/Api.csproj +++ b/Api/Api.csproj @@ -1,14 +1,30 @@ - + - net7.0 + net8.0 enable enable + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + diff --git a/Api/Config.cs b/Api/Config.cs new file mode 100644 index 0000000..53e1f68 --- /dev/null +++ b/Api/Config.cs @@ -0,0 +1,41 @@ +using IdentityServer4.Models; + +namespace Api +{ + public static class Config + { + public static IEnumerable GetApiResources() => + new List + { + new ApiResource("myresourceapi", "My Resource API") + { + Scopes = { "apiscope" } + } + }; + + public static IEnumerable GetApiScopes() => + new List + { + new ApiScope("apiscope", "API Scope") + }; + + public static IEnumerable GetClients() => + new List + { + new Client + { + ClientId = "secret_client_id", + AllowedGrantTypes = GrantTypes.ClientCredentials, + ClientSecrets = { new Secret("secret".Sha256()) }, + AllowedScopes = { "apiscope" } + } + }; + + public static IEnumerable GetIdentityResources() => + new List + { + new IdentityResources.OpenId(), + new IdentityResources.Profile() + }; + } +} diff --git a/Api/Controllers/AccountController.cs b/Api/Controllers/AccountController.cs new file mode 100644 index 0000000..8a6b744 --- /dev/null +++ b/Api/Controllers/AccountController.cs @@ -0,0 +1,96 @@ +using Api.DTOs.Account; +using Api.Entities; +using Api.Services; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using System.Security.Claims; + +namespace Api.Controllers +{ + [Route("api/[controller]")] + [ApiController] + public class AccountController : ControllerBase + { + private readonly JWTService _jwtService; + private readonly SignInManager _signInManager; + private readonly UserManager _userManager; + + public AccountController(JWTService jwtService, + SignInManager signInManager, + UserManager userManager) + { + _jwtService = jwtService; + _signInManager = signInManager; + _userManager = userManager; + } + + [Authorize] + [HttpGet("refresh-user-token")] + public async Task> RefreshUserToken() + { + var user = await _userManager.FindByNameAsync(User.FindFirst(ClaimTypes.Email)?.Value); + return await CreateApplicationUserDto(user); + } + + [HttpPost("login")] + public async Task> Login(LoginDto model) + { + var user = await _userManager.FindByNameAsync(model.UserName); + if (user == null) return Unauthorized("Invalid UserName or Password"); + + if (user.EmailConfirmed == false) return Unauthorized("Please confirm your email"); + + var result = await _signInManager.CheckPasswordSignInAsync(user, model.Password, false); + + if (!result.Succeeded) return Unauthorized("Invalid Username or Password"); + + return await CreateApplicationUserDto(user); + } + + [HttpPost("register")] + public async Task Register(RegisterDto model) + { + if(await CheckEmailExistsAsync(model.Email)) + { + return BadRequest($"An existing account is using {model.Email} email adress. Please try with another email adress"); + } + + var userToAdd = new User + { + FirstName = model.FirstName.ToLower(), + LastName = model.LastName.ToLower(), + UserName = model.Email.ToLower(), + Email = model.Email.ToLower(), + EmailConfirmed = true + }; + + var result = await _userManager.CreateAsync(userToAdd, model.Password); + + if (!result.Succeeded) return BadRequest(result.Errors); + + await _userManager.AddToRoleAsync(userToAdd, "User"); + + return Ok("Your account has been created successfully, you can login"); + } + + #region Private Helper Methods + private async Task CreateApplicationUserDto(User user) + { + return new UserDto + { + FirstName = user.FirstName, + LastName = user.LastName, + JWT = await _jwtService.CreateJWT(user) + }; + } + + private async Task CheckEmailExistsAsync(string email) + { + return await _userManager.Users.AnyAsync(x => x.Email == email.ToLower()); + } + #endregion + } +} diff --git a/Api/Controllers/WeatherForecastController.cs b/Api/Controllers/WeatherForecastController.cs deleted file mode 100644 index 515f63f..0000000 --- a/Api/Controllers/WeatherForecastController.cs +++ /dev/null @@ -1,33 +0,0 @@ -using Microsoft.AspNetCore.Mvc; - -namespace Api.Controllers -{ - [ApiController] - [Route("[controller]")] - public class WeatherForecastController : ControllerBase - { - private static readonly string[] Summaries = new[] - { - "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" - }; - - private readonly ILogger _logger; - - public WeatherForecastController(ILogger logger) - { - _logger = logger; - } - - [HttpGet(Name = "GetWeatherForecast")] - public IEnumerable Get() - { - return Enumerable.Range(1, 5).Select(index => new WeatherForecast - { - Date = DateOnly.FromDateTime(DateTime.Now.AddDays(index)), - TemperatureC = Random.Shared.Next(-20, 55), - Summary = Summaries[Random.Shared.Next(Summaries.Length)] - }) - .ToArray(); - } - } -} \ No newline at end of file diff --git a/Api/DTOs/Account/LoginDto.cs b/Api/DTOs/Account/LoginDto.cs new file mode 100644 index 0000000..1b9e7cc --- /dev/null +++ b/Api/DTOs/Account/LoginDto.cs @@ -0,0 +1,12 @@ +using System.ComponentModel.DataAnnotations; + +namespace Api.DTOs.Account +{ + public class LoginDto + { + [Required] + public string UserName { get; set; } + [Required] + public string Password { get; set; } + } +} diff --git a/Api/DTOs/Account/RegisterDto.cs b/Api/DTOs/Account/RegisterDto.cs new file mode 100644 index 0000000..4c97787 --- /dev/null +++ b/Api/DTOs/Account/RegisterDto.cs @@ -0,0 +1,20 @@ +using System.ComponentModel.DataAnnotations; + +namespace Api.DTOs.Account +{ + public class RegisterDto + { + [Required] + [StringLength(15,MinimumLength =3,ErrorMessage = "First name must be at least {2}, and maximum {1} characters")] + public string FirstName { get; set; } + [Required] + [StringLength(15, MinimumLength = 3, ErrorMessage = "Last name must be at least {2}, and maximum {1} characters")] + public string LastName { get; set; } + [Required] + [RegularExpression("^\\w+@[a-zA-Z_]+?\\.[a-zA-Z]{2,3}$", ErrorMessage = "Invalid email adress")] + public string Email { get; set; } + [Required] + [StringLength(15, MinimumLength = 6, ErrorMessage = "Password must be at least {2}, and maximum {1} characters")] + public string Password { get; set; } + } +} diff --git a/Api/DTOs/Account/UserDto.cs b/Api/DTOs/Account/UserDto.cs new file mode 100644 index 0000000..e907f23 --- /dev/null +++ b/Api/DTOs/Account/UserDto.cs @@ -0,0 +1,9 @@ +namespace Api.DTOs.Account +{ + public class UserDto + { + public string FirstName { get; set; } + public string LastName { get; set; } + public string JWT { get; set; } + } +} diff --git a/Api/Database/Context.cs b/Api/Database/Context.cs new file mode 100644 index 0000000..2e4a6b9 --- /dev/null +++ b/Api/Database/Context.cs @@ -0,0 +1,21 @@ +using Api.Entities; +using Microsoft.AspNetCore.Identity.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore; + +namespace Api.Database +{ + public class Context : IdentityDbContext + { + public Context(DbContextOptions options) : base(options) + { + + } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + modelBuilder.SeedRoles(); + modelBuilder.SeedAdmin(); + } + } +} diff --git a/Api/Database/DataSeed.cs b/Api/Database/DataSeed.cs new file mode 100644 index 0000000..14cddd5 --- /dev/null +++ b/Api/Database/DataSeed.cs @@ -0,0 +1,47 @@ +using Api.Entities; +using Microsoft.AspNetCore.Identity; +using Microsoft.EntityFrameworkCore; + +namespace Api.Database +{ + public static class DataSeed + { + public static void SeedRoles(this ModelBuilder modelBuilder) + { + modelBuilder.Entity().HasData(new[] + { + new IdentityRole { Id = "1", Name = "Admin", NormalizedName = "ADMIN" }, + new IdentityRole { Id = "2", Name = "User", NormalizedName = "USER" } + }); + } + + public static void SeedAdmin(this ModelBuilder modelBuilder) + { + const string adminId = "1"; + const string adminEmail = "vladusmoroz@gmail.com"; + var hasher = new PasswordHasher(); + var admin = new User + { + Id = adminId, + EmailConfirmed = true, + UserName = adminEmail, + NormalizedUserName = adminEmail.ToUpper(), + FirstName = "Vladyslav", + LastName = "Moroz", + Email = adminEmail, + NormalizedEmail = adminEmail.ToUpper(), + PhoneNumber = "+380955638293", + SecurityStamp = Guid.NewGuid().ToString() + }; + admin.PasswordHash = hasher.HashPassword(admin, "qwerty12345"); + + modelBuilder.Entity().HasData(admin); + + modelBuilder.Entity>().HasData(new[] + { + new IdentityUserRole { UserId = adminId, RoleId = "1" }, + new IdentityUserRole { UserId = adminId, RoleId = "2" } + }); + } + } +} diff --git a/Api/Database/Migrations/20250316104624_AddingAdminToDatabase.Designer.cs b/Api/Database/Migrations/20250316104624_AddingAdminToDatabase.Designer.cs new file mode 100644 index 0000000..f648f8d --- /dev/null +++ b/Api/Database/Migrations/20250316104624_AddingAdminToDatabase.Designer.cs @@ -0,0 +1,338 @@ +// +using System; +using Api.Database; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace Api.Database.Migrations +{ + [DbContext(typeof(Context))] + [Migration("20250316104624_AddingAdminToDatabase")] + partial class AddingAdminToDatabase + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "7.0.20") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("Api.Entities.User", b => + { + b.Property("Id") + .HasColumnType("nvarchar(450)"); + + b.Property("AccessFailedCount") + .HasColumnType("int"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("nvarchar(max)"); + + b.Property("DateCreated") + .HasColumnType("datetime2"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("EmailConfirmed") + .HasColumnType("bit"); + + b.Property("FirstName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("LastName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("LockoutEnabled") + .HasColumnType("bit"); + + b.Property("LockoutEnd") + .HasColumnType("datetimeoffset"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("PasswordHash") + .HasColumnType("nvarchar(max)"); + + b.Property("PhoneNumber") + .HasColumnType("nvarchar(max)"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("bit"); + + b.Property("SecurityStamp") + .HasColumnType("nvarchar(max)"); + + b.Property("TwoFactorEnabled") + .HasColumnType("bit"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasDatabaseName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex") + .HasFilter("[NormalizedUserName] IS NOT NULL"); + + b.ToTable("AspNetUsers", (string)null); + + b.HasData( + new + { + Id = "1", + AccessFailedCount = 0, + ConcurrencyStamp = "a87eae14-f4cd-462a-a1bf-b3709efd8226", + DateCreated = new DateTime(2025, 3, 16, 10, 46, 23, 803, DateTimeKind.Utc).AddTicks(590), + Email = "vladusmoroz@gmail.com", + EmailConfirmed = true, + FirstName = "Vladyslav", + LastName = "Moroz", + LockoutEnabled = false, + NormalizedEmail = "VLADUSMOROZ@GMAIL.COM", + NormalizedUserName = "VLADUSMOROZ@GMAIL.COM", + PasswordHash = "AQAAAAIAAYagAAAAEFuv8IeZ7qhXPhiN6bZ0PX4wumsJdJMjRq3X9TieyPdeFbP1QpOkRRoPKvHKC+pTJQ==", + PhoneNumber = "+380955638293", + PhoneNumberConfirmed = false, + SecurityStamp = "eef79b5f-0801-4d55-92c6-769d14f34ab8", + TwoFactorEnabled = false, + UserName = "vladusmoroz@gmail.com" + }); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => + { + b.Property("Id") + .HasColumnType("nvarchar(450)"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("NormalizedName") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasDatabaseName("RoleNameIndex") + .HasFilter("[NormalizedName] IS NOT NULL"); + + b.ToTable("AspNetRoles", (string)null); + + b.HasData( + new + { + Id = "1", + Name = "Admin", + NormalizedName = "ADMIN" + }, + new + { + Id = "2", + Name = "User", + NormalizedName = "USER" + }); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("nvarchar(max)"); + + b.Property("ClaimValue") + .HasColumnType("nvarchar(max)"); + + b.Property("RoleId") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("nvarchar(max)"); + + b.Property("ClaimValue") + .HasColumnType("nvarchar(max)"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider") + .HasColumnType("nvarchar(450)"); + + b.Property("ProviderKey") + .HasColumnType("nvarchar(450)"); + + b.Property("ProviderDisplayName") + .HasColumnType("nvarchar(max)"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.Property("UserId") + .HasColumnType("nvarchar(450)"); + + b.Property("RoleId") + .HasColumnType("nvarchar(450)"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles", (string)null); + + b.HasData( + new + { + UserId = "1", + RoleId = "1" + }, + new + { + UserId = "1", + RoleId = "2" + }); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("nvarchar(450)"); + + b.Property("LoginProvider") + .HasColumnType("nvarchar(450)"); + + b.Property("Name") + .HasColumnType("nvarchar(450)"); + + b.Property("Value") + .HasColumnType("nvarchar(max)"); + + 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("Api.Entities.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("Api.Entities.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Api.Entities.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("Api.Entities.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Api/Database/Migrations/20250316104624_AddingAdminToDatabase.cs b/Api/Database/Migrations/20250316104624_AddingAdminToDatabase.cs new file mode 100644 index 0000000..49eebde --- /dev/null +++ b/Api/Database/Migrations/20250316104624_AddingAdminToDatabase.cs @@ -0,0 +1,252 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +#pragma warning disable CA1814 // Prefer jagged arrays over multidimensional + +namespace Api.Database.Migrations +{ + /// + public partial class AddingAdminToDatabase : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "AspNetRoles", + columns: table => new + { + Id = table.Column(type: "nvarchar(450)", nullable: false), + Name = table.Column(type: "nvarchar(256)", maxLength: 256, nullable: true), + NormalizedName = table.Column(type: "nvarchar(256)", maxLength: 256, nullable: true), + ConcurrencyStamp = table.Column(type: "nvarchar(max)", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetRoles", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "AspNetUsers", + columns: table => new + { + Id = table.Column(type: "nvarchar(450)", nullable: false), + FirstName = table.Column(type: "nvarchar(max)", nullable: false), + LastName = table.Column(type: "nvarchar(max)", nullable: false), + DateCreated = table.Column(type: "datetime2", nullable: false), + UserName = table.Column(type: "nvarchar(256)", maxLength: 256, nullable: true), + NormalizedUserName = table.Column(type: "nvarchar(256)", maxLength: 256, nullable: true), + Email = table.Column(type: "nvarchar(256)", maxLength: 256, nullable: true), + NormalizedEmail = table.Column(type: "nvarchar(256)", maxLength: 256, nullable: true), + EmailConfirmed = table.Column(type: "bit", nullable: false), + PasswordHash = table.Column(type: "nvarchar(max)", nullable: true), + SecurityStamp = table.Column(type: "nvarchar(max)", nullable: true), + ConcurrencyStamp = table.Column(type: "nvarchar(max)", nullable: true), + PhoneNumber = table.Column(type: "nvarchar(max)", nullable: true), + PhoneNumberConfirmed = table.Column(type: "bit", nullable: false), + TwoFactorEnabled = table.Column(type: "bit", nullable: false), + LockoutEnd = table.Column(type: "datetimeoffset", nullable: true), + LockoutEnabled = table.Column(type: "bit", nullable: false), + AccessFailedCount = table.Column(type: "int", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetUsers", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "AspNetRoleClaims", + columns: table => new + { + Id = table.Column(type: "int", nullable: false) + .Annotation("SqlServer:Identity", "1, 1"), + RoleId = table.Column(type: "nvarchar(450)", nullable: false), + ClaimType = table.Column(type: "nvarchar(max)", nullable: true), + ClaimValue = table.Column(type: "nvarchar(max)", 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: "int", nullable: false) + .Annotation("SqlServer:Identity", "1, 1"), + UserId = table.Column(type: "nvarchar(450)", nullable: false), + ClaimType = table.Column(type: "nvarchar(max)", nullable: true), + ClaimValue = table.Column(type: "nvarchar(max)", 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: "nvarchar(450)", nullable: false), + ProviderKey = table.Column(type: "nvarchar(450)", nullable: false), + ProviderDisplayName = table.Column(type: "nvarchar(max)", nullable: true), + UserId = table.Column(type: "nvarchar(450)", 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: "nvarchar(450)", nullable: false), + RoleId = table.Column(type: "nvarchar(450)", 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: "nvarchar(450)", nullable: false), + LoginProvider = table.Column(type: "nvarchar(450)", nullable: false), + Name = table.Column(type: "nvarchar(450)", nullable: false), + Value = table.Column(type: "nvarchar(max)", 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.InsertData( + table: "AspNetRoles", + columns: new[] { "Id", "ConcurrencyStamp", "Name", "NormalizedName" }, + values: new object[,] + { + { "1", null, "Admin", "ADMIN" }, + { "2", null, "User", "USER" } + }); + + migrationBuilder.InsertData( + table: "AspNetUsers", + columns: new[] { "Id", "AccessFailedCount", "ConcurrencyStamp", "DateCreated", "Email", "EmailConfirmed", "FirstName", "LastName", "LockoutEnabled", "LockoutEnd", "NormalizedEmail", "NormalizedUserName", "PasswordHash", "PhoneNumber", "PhoneNumberConfirmed", "SecurityStamp", "TwoFactorEnabled", "UserName" }, + values: new object[] { "1", 0, "a87eae14-f4cd-462a-a1bf-b3709efd8226", new DateTime(2025, 3, 16, 10, 46, 23, 803, DateTimeKind.Utc).AddTicks(590), "vladusmoroz@gmail.com", true, "Vladyslav", "Moroz", false, null, "VLADUSMOROZ@GMAIL.COM", "VLADUSMOROZ@GMAIL.COM", "AQAAAAIAAYagAAAAEFuv8IeZ7qhXPhiN6bZ0PX4wumsJdJMjRq3X9TieyPdeFbP1QpOkRRoPKvHKC+pTJQ==", "+380955638293", false, "eef79b5f-0801-4d55-92c6-769d14f34ab8", false, "vladusmoroz@gmail.com" }); + + migrationBuilder.InsertData( + table: "AspNetUserRoles", + columns: new[] { "RoleId", "UserId" }, + values: new object[,] + { + { "1", "1" }, + { "2", "1" } + }); + + migrationBuilder.CreateIndex( + name: "IX_AspNetRoleClaims_RoleId", + table: "AspNetRoleClaims", + column: "RoleId"); + + migrationBuilder.CreateIndex( + name: "RoleNameIndex", + table: "AspNetRoles", + column: "NormalizedName", + unique: true, + filter: "[NormalizedName] IS NOT NULL"); + + 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, + filter: "[NormalizedUserName] IS NOT NULL"); + } + + /// + 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/Api/Database/Migrations/ContextModelSnapshot.cs b/Api/Database/Migrations/ContextModelSnapshot.cs new file mode 100644 index 0000000..4f111d8 --- /dev/null +++ b/Api/Database/Migrations/ContextModelSnapshot.cs @@ -0,0 +1,335 @@ +// +using System; +using Api.Database; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace Api.Database.Migrations +{ + [DbContext(typeof(Context))] + partial class ContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "7.0.20") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("Api.Entities.User", b => + { + b.Property("Id") + .HasColumnType("nvarchar(450)"); + + b.Property("AccessFailedCount") + .HasColumnType("int"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("nvarchar(max)"); + + b.Property("DateCreated") + .HasColumnType("datetime2"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("EmailConfirmed") + .HasColumnType("bit"); + + b.Property("FirstName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("LastName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("LockoutEnabled") + .HasColumnType("bit"); + + b.Property("LockoutEnd") + .HasColumnType("datetimeoffset"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("PasswordHash") + .HasColumnType("nvarchar(max)"); + + b.Property("PhoneNumber") + .HasColumnType("nvarchar(max)"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("bit"); + + b.Property("SecurityStamp") + .HasColumnType("nvarchar(max)"); + + b.Property("TwoFactorEnabled") + .HasColumnType("bit"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasDatabaseName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex") + .HasFilter("[NormalizedUserName] IS NOT NULL"); + + b.ToTable("AspNetUsers", (string)null); + + b.HasData( + new + { + Id = "1", + AccessFailedCount = 0, + ConcurrencyStamp = "a87eae14-f4cd-462a-a1bf-b3709efd8226", + DateCreated = new DateTime(2025, 3, 16, 10, 46, 23, 803, DateTimeKind.Utc).AddTicks(590), + Email = "vladusmoroz@gmail.com", + EmailConfirmed = true, + FirstName = "Vladyslav", + LastName = "Moroz", + LockoutEnabled = false, + NormalizedEmail = "VLADUSMOROZ@GMAIL.COM", + NormalizedUserName = "VLADUSMOROZ@GMAIL.COM", + PasswordHash = "AQAAAAIAAYagAAAAEFuv8IeZ7qhXPhiN6bZ0PX4wumsJdJMjRq3X9TieyPdeFbP1QpOkRRoPKvHKC+pTJQ==", + PhoneNumber = "+380955638293", + PhoneNumberConfirmed = false, + SecurityStamp = "eef79b5f-0801-4d55-92c6-769d14f34ab8", + TwoFactorEnabled = false, + UserName = "vladusmoroz@gmail.com" + }); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => + { + b.Property("Id") + .HasColumnType("nvarchar(450)"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("NormalizedName") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasDatabaseName("RoleNameIndex") + .HasFilter("[NormalizedName] IS NOT NULL"); + + b.ToTable("AspNetRoles", (string)null); + + b.HasData( + new + { + Id = "1", + Name = "Admin", + NormalizedName = "ADMIN" + }, + new + { + Id = "2", + Name = "User", + NormalizedName = "USER" + }); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("nvarchar(max)"); + + b.Property("ClaimValue") + .HasColumnType("nvarchar(max)"); + + b.Property("RoleId") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("nvarchar(max)"); + + b.Property("ClaimValue") + .HasColumnType("nvarchar(max)"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider") + .HasColumnType("nvarchar(450)"); + + b.Property("ProviderKey") + .HasColumnType("nvarchar(450)"); + + b.Property("ProviderDisplayName") + .HasColumnType("nvarchar(max)"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.Property("UserId") + .HasColumnType("nvarchar(450)"); + + b.Property("RoleId") + .HasColumnType("nvarchar(450)"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles", (string)null); + + b.HasData( + new + { + UserId = "1", + RoleId = "1" + }, + new + { + UserId = "1", + RoleId = "2" + }); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("nvarchar(450)"); + + b.Property("LoginProvider") + .HasColumnType("nvarchar(450)"); + + b.Property("Name") + .HasColumnType("nvarchar(450)"); + + b.Property("Value") + .HasColumnType("nvarchar(max)"); + + 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("Api.Entities.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("Api.Entities.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Api.Entities.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("Api.Entities.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Api/Entities/User.cs b/Api/Entities/User.cs new file mode 100644 index 0000000..3f6ddc1 --- /dev/null +++ b/Api/Entities/User.cs @@ -0,0 +1,15 @@ +using Microsoft.AspNetCore.Identity; +using System.ComponentModel.DataAnnotations; +using System.Reflection.Metadata.Ecma335; + +namespace Api.Entities +{ + public class User : IdentityUser + { + [Required] + public string FirstName { get; set; } + [Required] + public string LastName { get; set; } + public DateTime DateCreated { get; set; } = DateTime.UtcNow; + } +} diff --git a/Api/Program.cs b/Api/Program.cs index 48863a6..c40d38e 100644 --- a/Api/Program.cs +++ b/Api/Program.cs @@ -1,14 +1,105 @@ +using Api; +using Api.Database; +using Api.Entities; +using Api.Services; +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.AspNetCore.Identity; +using Microsoft.EntityFrameworkCore; +using Microsoft.IdentityModel.Tokens; +using System.Text; + var builder = WebApplication.CreateBuilder(args); -// Add services to the container. + builder.Services.AddControllers(); -// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle + builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(); +builder.Services.AddDbContext(options => +{ + options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection")); +}); + +//builder.Services.AddIdentityServer() +// .AddDeveloperSigningCredential() +// .AddInMemoryApiResources(Config.GetApiResources()) +// .AddInMemoryClients(Config.GetClients()); + +builder.Services.AddIdentityServer() + .AddDeveloperSigningCredential() + .AddInMemoryApiResources(Config.GetApiResources()) // ÏîâåðòຠAPI ðåñóðñè + .AddInMemoryApiScopes(Config.GetApiScopes()) // ÏîâåðòຠAPI scopes + .AddInMemoryClients(Config.GetClients()) // Ïîâåðòຠê볺íò³â + .AddInMemoryIdentityResources(Config.GetIdentityResources()); // Ïîâåðòຠðåñóðñè ³äåíòèô³êàö³¿ (ÿêùî ïîòð³áíî) + +// be able to inject JWTService class inside our Controllers +builder.Services.AddScoped(); + +// defining our IdentityCore Service +builder.Services.AddIdentityCore(options => +{ + // password configuration + options.Password.RequiredLength = 6; + options.Password.RequireDigit = false; + options.Password.RequireLowercase = false; + options.Password.RequireUppercase = false; + options.Password.RequireNonAlphanumeric = false; + + // for email configuration + options.SignIn.RequireConfirmedEmail = true; +}) + .AddRoles() // be able to add roles + .AddRoleManager>() // be able to make use of RoleManager + .AddEntityFrameworkStores() // providing our context + .AddSignInManager>() // make use of SignIn manager + .AddUserManager>() // make use of UserManager to create users + .AddDefaultTokenProviders(); // be able to create tokens for email confirmation + +// be able to authenticate users using JWT +builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) + .AddJwtBearer(options => + { + options.TokenValidationParameters = new Microsoft.IdentityModel.Tokens.TokenValidationParameters + { + // validate the token based on the key we have provided inside appsettings.development.json JWT:KEy + ValidateIssuerSigningKey = true, + // the issuer signing key based on JWT:Key + IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(builder.Configuration["JWT:Key"])), + // the issuer which in here is the api project url we are using + ValidIssuer = builder.Configuration["JWT:Issuer"], + // validate the issuer ( who ever is issuing the JWT ) + ValidateIssuer = true, + // don't validate audience ( angular side ) + ValidateAudience = false, + ValidAudience = builder.Configuration["JWT:Audience"] + }; + }); + + + +builder.Services.AddAuthorization(options => +{ + options.AddPolicy("AdminPolicy", policy => policy.RequireRole("Admin")); + options.AddPolicy("UserPolicy", policy => policy.RequireRole("User")); + +}); + +builder.Services.AddCors(options => +{ + options.AddPolicy("AllowAll", policy => + { + policy.AllowAnyOrigin() + .AllowAnyMethod() + .AllowAnyHeader(); + }); +}); + var app = builder.Build(); +app.UseCors("AllowAll"); + // Configure the HTTP request pipeline. if (app.Environment.IsDevelopment()) { @@ -16,8 +107,10 @@ app.UseSwaggerUI(); } +app.UseIdentityServer(); app.UseHttpsRedirection(); +app.UseAuthentication(); app.UseAuthorization(); app.MapControllers(); diff --git a/Api/Properties/launchSettings.json b/Api/Properties/launchSettings.json index 568f54e..0623027 100644 --- a/Api/Properties/launchSettings.json +++ b/Api/Properties/launchSettings.json @@ -12,23 +12,13 @@ "http": { "commandName": "Project", "dotnetRunMessages": true, - "launchBrowser": true, + "launchBrowser": false, "launchUrl": "swagger", "applicationUrl": "http://localhost:5085", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } }, - "https": { - "commandName": "Project", - "dotnetRunMessages": true, - "launchBrowser": true, - "launchUrl": "swagger", - "applicationUrl": "https://localhost:7188;http://localhost:5085", - "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development" - } - }, "IIS Express": { "commandName": "IISExpress", "launchBrowser": true, diff --git a/Api/Services/JWTService.cs b/Api/Services/JWTService.cs new file mode 100644 index 0000000..f347bc7 --- /dev/null +++ b/Api/Services/JWTService.cs @@ -0,0 +1,50 @@ +using Api.Entities; +using Microsoft.AspNetCore.Identity; +using Microsoft.IdentityModel.Tokens; +using System.IdentityModel.Tokens.Jwt; +using System.Security.Claims; +using System.Text; + +namespace Api.Services +{ + public class JWTService + { + private readonly IConfiguration _config; + private readonly UserManager _userManager; + private readonly SymmetricSecurityKey _jwtKey; + public JWTService(IConfiguration config, UserManager userManager) + { + _config = config; + _userManager = userManager; + _jwtKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_config["JWT:Key"])); + } + public async Task CreateJWT(User user) + { + var userClaims = new List + { + new Claim(ClaimTypes.NameIdentifier, user.Id), + new Claim(ClaimTypes.Email, user.Email), + new Claim(ClaimTypes.GivenName, user.FirstName), + new Claim(ClaimTypes.Surname, user.LastName), + }; + var userRoles = await _userManager.GetRolesAsync(user); // ???? + + userClaims.AddRange(userRoles.Select(role => new Claim(ClaimTypes.Role, role))); + + var credentials = new SigningCredentials(_jwtKey, SecurityAlgorithms.HmacSha256Signature); + var tokenDescriptor = new SecurityTokenDescriptor + { + Subject = new ClaimsIdentity(userClaims), + Expires = DateTime.UtcNow.AddDays(int.Parse(_config["JWT:ExpiresInDays"])), + SigningCredentials = credentials, + Issuer = _config["JWT:Issuer"], + Audience = "timetracker_api" + }; + + var tokenHandler = new JwtSecurityTokenHandler(); + var jwt = tokenHandler.CreateToken(tokenDescriptor); + + return tokenHandler.WriteToken(jwt); + } + } +} diff --git a/Api/WeatherForecast.cs b/Api/WeatherForecast.cs deleted file mode 100644 index 0ef35b4..0000000 --- a/Api/WeatherForecast.cs +++ /dev/null @@ -1,13 +0,0 @@ -namespace Api -{ - public class WeatherForecast - { - public DateOnly Date { get; set; } - - public int TemperatureC { get; set; } - - public int TemperatureF => 32 + (int)(TemperatureC / 0.5556); - - public string? Summary { get; set; } - } -} \ No newline at end of file diff --git a/Api/appsettings.json b/Api/appsettings.json index 10f68b8..f526562 100644 --- a/Api/appsettings.json +++ b/Api/appsettings.json @@ -1,3 +1,28 @@ +//{ +// "Logging": { +// "LogLevel": { +// "Default": "Information", +// "Microsoft.AspNetCore": "Warning" +// } +// }, +// "AllowedHosts": "*", +// "ConnectionStrings": { +// "DefaultConnection": "Server=VLADYMOR;Database=IdentityServer;Trusted_connection=True;MultipleActiveResultSets=true;TrustServerCertificate=true" +// }, +// "JWT": { +// "Key": "BB86C512D863772FD45ABAF5F1C55ATSVHH12AT5VKAEPTY2051AWRH3772FD45ABAF5", +// "ExpiresInDays": 15, +// "Issuer": "http://localhost:5085", +// "Audience": "timetracker_api" +// }, +// "IdentityServer": { +// "Clients": { +// "timetracker_client": { +// "Profile": "IdentityServerSPA" +// } +// } +// } +//} { "Logging": { "LogLevel": { @@ -5,5 +30,39 @@ "Microsoft.AspNetCore": "Warning" } }, - "AllowedHosts": "*" -} + "AllowedHosts": "*", + "ConnectionStrings": { + "DefaultConnection": "Server=VLADYMOR;Database=IdentityServer;Trusted_Connection=True;MultipleActiveResultSets=true;TrustServerCertificate=true" + }, + "JWT": { + "Key": "BB86C512D863772FD45ABAF5F1C55ATSVHH12AT5VKAEPTY2051AWRH3772FD45ABAF5", + "ExpiresInDays": 15, + "Issuer": "http://localhost:5085", + "Audience": "timetracker_api" + }, + "IdentityServer": { + "Clients": { + "timetracker_client": { + "ClientId": "secret_client_id", + "ClientSecrets": [ "secret" ], + "AllowedGrantTypes": [ "client_credentials" ], + "AllowedScopes": [ "apiscope" ] + } + }, + "Resources": { + "ApiResources": [ + { + "Name": "timetracker_api", + "DisplayName": "Time Tracker API", + "Scopes": [ "apiscope" ] + } + ], + "ApiScopes": [ + { + "Name": "apiscope", + "DisplayName": "API Scope" + } + ] + } + } +} \ No newline at end of file diff --git a/Api/tempkey.jwk b/Api/tempkey.jwk new file mode 100644 index 0000000..4154322 --- /dev/null +++ b/Api/tempkey.jwk @@ -0,0 +1 @@ +{"AdditionalData":{},"Alg":"RS256","Crv":null,"D":"Al4uPVTKQW-0_sIgA93UGnwaqcjAhWYv8ko68w9fZhhDhoSGMQSIoOHBP4BVCBZiA2D7140WiPq-BCep60t7U-9j5rZ2DRLi20Povblu5DKHmQj5NOrSeWE1eYaX-OoOxQQZP-6DFoizfR0GQt-JxiKwyB4xx6u1hN_CIyyNpNPeV6XxqpBLg1QGIBZzB8J0z4M2Iwk2P-4_yjRjX1pAf9DielUjbWtbd7-_iwO_KH7UmfpkmBkZVAnjdx9yUIF1iiALoW2C-9evTM1hGWW6KvlG34Sw3D08Krg5rwTlq5o9jgZIbz1iYV3fKlyBtXait01E0PggTL13nHgYHiLMhQ","DP":"FmN3K6Dv7I2QIPuvDasdlwvX2pB4R95RicYHdTrAQa76k0lQzEChIvUIxUc_RTZrng4ZW9BetSoW60qwwTPHPRucuNfVU9ROKQXfGUCwfV6_KcLzaMc0lC9P5BsGjbCsi3cbElSiGsluwHyZvHHmWN1p6xxfmNzPnQHiIYQbC08","DQ":"Uh4s6HVrB3xULGB4dqFeaLJVgQgUZrV3pCmXWeRKNpjhQuifAf-z129qbJOkwiTA6w7uM5xtdolJZqT2A0wtCo7ofXDq4k93gb8QOisNqxsBlJXQKyZdLEESYmha6-O9lmHHfdU2iZFGVE27AP-9374aj8WuQQsXPTpiyXQ1-g8","E":"AQAB","K":null,"KeyId":"83C1833FEB7A86335C45C340F87A1686","KeyOps":[],"Kid":"83C1833FEB7A86335C45C340F87A1686","Kty":"RSA","N":"zHWUlBQZCRgXxFdH1sxil9lDFQ1B0l44JoKcQuQo-CDtCcxMEnVBktlBhRYacyd_E4_CHqtezNN6EBTvy9sbzEzYkoEZ0GvocOlnbFPaxiD8JJ6bHVqGTtFny2gNU1Halgw7X18fC7vgO4kQfhPH5oXf5Kv7O4b6dnsc-9kmlD-R_qUfb7zcOzDakuZMB6vF1nSIhE0SgSK_JntcYoVmo_plEkcswaSS7hK81W9nkCEmxMgQ2zQUYmqlis0D4460o9Gtldtl38IInQUhEHpY2sxNOOIgbkQpRYjexvnbZPLC9uQ5QnlV4mPMyPJJhnuhUZi4FFx7YMoUMnUqhsS5TQ","Oth":[],"P":"2YkceBc1_T9yc8CxrR1i4Fzep2O8wh6k86YFDsWlJ46aaDe4QB7fFAIddtWzutYEMWfINEpVdG9vrN2nQmbfR3_vxeAgBTlFsjIHLZMjVmRyVjxkMDmW_viaEkCy3dmZdBaQqM7xI1FRaInu9do6U5DTC1v9UHCqaCB0uUz0P_8","Q":"8JyP98kxgwgK0AREbvM6M2YD8-6HHGUFemZuWdewvTSc7_autc6w9ZxJKLwtnnq1Cq1Gf6WMLul2ldkGPZ_W357LY6vnFf2eBKFnN7JaMRvPDf2mn0fem_stZeg7Skbo8FjKzwKaM6mb0yJ0S0A1GdUDsglW-2z_gvFiZQGEBrM","QI":"SPOUmw6Me0aXrhZtBvJ-KWkIL8dU3ESkP5_b4RMdek7Ay-Z8pQhNlb8fT9SivlPGztskZjiNeT5-yvYO3PCfDr6NziMTdXkfbk410ymnlvRUuvcz-Q9dMEWr_xAXLHMTqktq5xJ7yHFAk6UP2o3CPY5iGagz9kURy4e_ZaheGNk","Use":null,"X":null,"X5c":[],"X5t":null,"X5tS256":null,"X5u":null,"Y":null,"KeySize":2048,"HasPrivateKey":true,"CryptoProviderFactory":{"CryptoProviderCache":{},"CustomCryptoProvider":null,"CacheSignatureProviders":true,"SignatureProviderObjectPoolCacheSize":32}} \ No newline at end of file