From 2274eddb4ef25bbbd3d428b2089ea37b05eb2e1c Mon Sep 17 00:00:00 2001 From: Jonatan Berg Romundgard Date: Wed, 27 Aug 2025 11:23:54 +0200 Subject: [PATCH] Another auth project --- .../Controllers/UsersController.cs | 101 ++++++++++++++ exercise.wwwapi/DTOs/CustomerPost.cs | 11 ++ exercise.wwwapi/DTOs/CustomerPut.cs | 9 ++ exercise.wwwapi/Data/CinemaContext.cs | 72 ++++++++++ .../DataTransfer/Requests/AuthRequest.cs | 13 ++ .../Requests/RegistrationRequest.cs | 19 +++ .../DataTransfer/Response/AuthResponse.cs | 9 ++ exercise.wwwapi/Endpoints/CustomerEndpoint.cs | 123 ++++++++++++++++++ exercise.wwwapi/Enums/Role.cs | 8 ++ exercise.wwwapi/Models/ApplicationUser.cs | 10 ++ exercise.wwwapi/Models/Customer.cs | 17 +++ exercise.wwwapi/Program.cs | 107 ++++++++++++++- exercise.wwwapi/Repository/IRepository.cs | 14 ++ exercise.wwwapi/Repository/Repository.cs | 50 +++++++ exercise.wwwapi/Services/TokenService.cs | 85 ++++++++++++ exercise.wwwapi/exercise.wwwapi.csproj | 16 ++- 16 files changed, 660 insertions(+), 4 deletions(-) create mode 100644 exercise.wwwapi/Controllers/UsersController.cs create mode 100644 exercise.wwwapi/DTOs/CustomerPost.cs create mode 100644 exercise.wwwapi/DTOs/CustomerPut.cs create mode 100644 exercise.wwwapi/Data/CinemaContext.cs create mode 100644 exercise.wwwapi/DataTransfer/Requests/AuthRequest.cs create mode 100644 exercise.wwwapi/DataTransfer/Requests/RegistrationRequest.cs create mode 100644 exercise.wwwapi/DataTransfer/Response/AuthResponse.cs create mode 100644 exercise.wwwapi/Endpoints/CustomerEndpoint.cs create mode 100644 exercise.wwwapi/Enums/Role.cs create mode 100644 exercise.wwwapi/Models/ApplicationUser.cs create mode 100644 exercise.wwwapi/Models/Customer.cs create mode 100644 exercise.wwwapi/Repository/IRepository.cs create mode 100644 exercise.wwwapi/Repository/Repository.cs create mode 100644 exercise.wwwapi/Services/TokenService.cs diff --git a/exercise.wwwapi/Controllers/UsersController.cs b/exercise.wwwapi/Controllers/UsersController.cs new file mode 100644 index 0000000..d0f692f --- /dev/null +++ b/exercise.wwwapi/Controllers/UsersController.cs @@ -0,0 +1,101 @@ +using api_cinema_challenge.Data; +using api_cinema_challenge.DataTransfer.Requests; +using api_cinema_challenge.DataTransfer.Response; +using api_cinema_challenge.Enums; +using api_cinema_challenge.Models; +using api_cinema_challenge.Services; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Mvc; +using System.Data; + +namespace exercise.wwwapi.Controllers +{ + [Route("api/[controller]")] + [ApiController] + public class UsersController : ControllerBase + { + private readonly UserManager _userManager; + private readonly CinemaContext _context; + private readonly TokenService _tokenService; + + public UsersController(UserManager userManager, CinemaContext context, + TokenService tokenService, ILogger logger) + { + _userManager = userManager; + _context = context; + _tokenService = tokenService; + } + + + [HttpPost] + [Route("register")] + public async Task Register(RegistrationRequest request) + { + if (!ModelState.IsValid) + { + return BadRequest(ModelState); + } + + var result = await _userManager.CreateAsync( + new ApplicationUser { UserName = request.Username, Email = request.Email, Role = request.Role }, + request.Password! + ); + + if (result.Succeeded) + { + request.Password = ""; + return CreatedAtAction(nameof(Register), new { email = request.Email, role = Role.User }, request); + } + + foreach (var error in result.Errors) + { + ModelState.AddModelError(error.Code, error.Description); + } + + return BadRequest(ModelState); + } + + + [HttpPost] + [Route("login")] + public async Task> Authenticate([FromBody] AuthRequest request) + { + if (!ModelState.IsValid) + { + return BadRequest(ModelState); + } + + var managedUser = await _userManager.FindByEmailAsync(request.Email!); + + if (managedUser == null) + { + return BadRequest("Bad credentials"); + } + + var isPasswordValid = await _userManager.CheckPasswordAsync(managedUser, request.Password!); + + if (!isPasswordValid) + { + return BadRequest("Bad credentials"); + } + + var userInDb = _context.Users.FirstOrDefault(u => u.Email == request.Email); + + if (userInDb is null) + { + return Unauthorized(); + } + + var accessToken = _tokenService.CreateToken(userInDb); + await _context.SaveChangesAsync(); + + return Ok(new AuthResponse + { + Username = userInDb.UserName, + Email = userInDb.Email, + Token = accessToken, + }); + } + } +} diff --git a/exercise.wwwapi/DTOs/CustomerPost.cs b/exercise.wwwapi/DTOs/CustomerPost.cs new file mode 100644 index 0000000..6b500e8 --- /dev/null +++ b/exercise.wwwapi/DTOs/CustomerPost.cs @@ -0,0 +1,11 @@ +namespace api_cinema_challenge.DTOs +{ + public class CustomerPost + { + public string Name { get; set; } + public string Email { get; set; } + public string Phone { get; set; } + //public DateTime CreatedAt { get; set; } + //public DateTime UpdatedAt { get; set; } + } +} diff --git a/exercise.wwwapi/DTOs/CustomerPut.cs b/exercise.wwwapi/DTOs/CustomerPut.cs new file mode 100644 index 0000000..f02ae3e --- /dev/null +++ b/exercise.wwwapi/DTOs/CustomerPut.cs @@ -0,0 +1,9 @@ +namespace api_cinema_challenge.DTOs +{ + public class CustomerPut + { + public string Name { get; set; } + public string Email { get; set; } + public string Phone { get; set; } + } +} diff --git a/exercise.wwwapi/Data/CinemaContext.cs b/exercise.wwwapi/Data/CinemaContext.cs new file mode 100644 index 0000000..b8bee24 --- /dev/null +++ b/exercise.wwwapi/Data/CinemaContext.cs @@ -0,0 +1,72 @@ +using api_cinema_challenge.Models; +using Microsoft.AspNetCore.Identity.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore; +using Newtonsoft.Json.Linq; + +namespace api_cinema_challenge.Data +{ + public class CinemaContext : IdentityUserContext + { + private string _connectionString; + public CinemaContext(DbContextOptions options) : base(options) + { + var configuration = new ConfigurationBuilder().AddJsonFile("appsettings.json").Build(); + _connectionString = configuration.GetValue("ConnectionStrings:DefaultConnectionString")!; + this.Database.EnsureCreated(); + } + + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + { + optionsBuilder.UseNpgsql(_connectionString); + } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + // Customers + Customer customer = new Customer + { + Id = 1, + Name = "Jonatan", + Email = "jonnabr@hotmail.com", + Phone = "+4793277670", + CreatedAt = new DateTime(2025, 01, 20, 12, 00, 00, DateTimeKind.Utc), + UpdatedAt = new DateTime(2025, 01, 21, 15, 00, 00, DateTimeKind.Utc) + }; + + Customer customer2 = new Customer + { + Id = 2, + Name = "Isabel", + Email = "Isabel@hotmail.com", + Phone = "+4792088620", + CreatedAt = new DateTime(2025, 01, 20, 12, 00, 00, DateTimeKind.Utc), + UpdatedAt = new DateTime(2025, 01, 21, 15, 00, 00, DateTimeKind.Utc) + }; + + Customer customer3 = new Customer + { + Id = 3, + Name = "Marius", + Email = "marius@hotmail.com", + Phone = "+4798765432", + CreatedAt = new DateTime(2025, 01, 19, 12, 00, 00, DateTimeKind.Utc), + UpdatedAt = new DateTime(2025, 01, 21, 17, 00, 00, DateTimeKind.Utc) + }; + + Customer customer4 = new Customer + { + Id = 4, + Name = "Emma", + Email = "emma@hotmail.com", + Phone = "+4791234567", + CreatedAt = new DateTime(2025, 01, 18, 12, 00, 00, DateTimeKind.Utc), + UpdatedAt = new DateTime(2025, 01, 21, 18, 00, 00, DateTimeKind.Utc) + }; + + modelBuilder.Entity().HasData([customer, customer2, customer3, customer4]); + base.OnModelCreating(modelBuilder); + } + + public DbSet Customers { get; set; } + } +} diff --git a/exercise.wwwapi/DataTransfer/Requests/AuthRequest.cs b/exercise.wwwapi/DataTransfer/Requests/AuthRequest.cs new file mode 100644 index 0000000..606db05 --- /dev/null +++ b/exercise.wwwapi/DataTransfer/Requests/AuthRequest.cs @@ -0,0 +1,13 @@ +namespace api_cinema_challenge.DataTransfer.Requests +{ + public class AuthRequest + { + public string? Email { get; set; } + public string? Password { get; set; } + + public bool IsValid() + { + return true; + } + } +} diff --git a/exercise.wwwapi/DataTransfer/Requests/RegistrationRequest.cs b/exercise.wwwapi/DataTransfer/Requests/RegistrationRequest.cs new file mode 100644 index 0000000..693e199 --- /dev/null +++ b/exercise.wwwapi/DataTransfer/Requests/RegistrationRequest.cs @@ -0,0 +1,19 @@ +using api_cinema_challenge.Enums; +using System.ComponentModel.DataAnnotations; + +namespace api_cinema_challenge.DataTransfer.Requests +{ + public class RegistrationRequest + { + [Required] + public string? Email { get; set; } + + [Required] + public string? Username { get { return this.Email; } set { } } + + [Required] + public string? Password { get; set; } + + public Role Role { get; set; } = Role.User; + } +} diff --git a/exercise.wwwapi/DataTransfer/Response/AuthResponse.cs b/exercise.wwwapi/DataTransfer/Response/AuthResponse.cs new file mode 100644 index 0000000..b334a5a --- /dev/null +++ b/exercise.wwwapi/DataTransfer/Response/AuthResponse.cs @@ -0,0 +1,9 @@ +namespace api_cinema_challenge.DataTransfer.Response +{ + public class AuthResponse + { + public string? Username { get; set; } + public string? Email { get; set; } + public string? Token { get; set; } + } +} diff --git a/exercise.wwwapi/Endpoints/CustomerEndpoint.cs b/exercise.wwwapi/Endpoints/CustomerEndpoint.cs new file mode 100644 index 0000000..75b9952 --- /dev/null +++ b/exercise.wwwapi/Endpoints/CustomerEndpoint.cs @@ -0,0 +1,123 @@ +using api_cinema_challenge.DTOs; +using api_cinema_challenge.Models; +using api_cinema_challenge.Repository; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using System.Runtime.CompilerServices; + +namespace api_cinema_challenge.Endpoints +{ + public static class CustomerEndpoint + { + public static void ConfigureCustomerEndpoint(this WebApplication app) + { + var customerGroup = app.MapGroup("customers/").RequireAuthorization(); + customerGroup.MapGet("/", GetCustomers); + customerGroup.MapGet("/{id}", GetCustomerById); + customerGroup.MapPost("", CreateCustomer); + customerGroup.MapPut("", UpdateCustomer); + customerGroup.MapDelete("", DeleteCustomer); + } + + + [ProducesResponseType(StatusCodes.Status200OK)] + public static async Task GetCustomers(IRepository repository) + { + var entities = await repository.GetCustomers(); + List result = new List(); + foreach (var entity in entities) + { + result.Add(entity); + } + return TypedResults.Ok(new + { + status = "success", + data = result + }); + } + + [ProducesResponseType(StatusCodes.Status200OK)] + public static async Task GetCustomerById(IRepository repository, int id) + { + string statusString = "success"; + var entity = await repository.GetCustomerById(id); + if (entity == null) statusString = "NotFound"; + + return TypedResults.Ok(new + { + status = statusString, + data = entity + }); + } + + [ProducesResponseType(StatusCodes.Status201Created)] + public static async Task CreateCustomer(IRepository repository, CustomerPost model) + { + Customer customer = new Customer(); + customer.Name = model.Name; + customer.Email = model.Email; + customer.Phone = model.Phone; + customer.CreatedAt = DateTime.UtcNow; + customer.UpdatedAt = DateTime.UtcNow; + + try + { + var entity = await repository.CreateCustomer(customer); + return TypedResults.Created($"", new + { + status = "success", + data = entity + }); + } + catch (Exception e) + { + return TypedResults.Created($"", new + { + status = "failed", + data = new Customer() + }); + } + } + + [ProducesResponseType(StatusCodes.Status200OK)] + public static async Task UpdateCustomer(IRepository repository, int id, CustomerPut model) + { + var customer = await repository.GetCustomerById(id); + //if (entity == null) return TypedResults.NotFound(new {Error = $"Did not find a customer with id '{id}'."}); + if (customer == null) + { + return TypedResults.Ok(new + { + status = "NotFound", + data = customer + }); + } + + if (model.Name != null) customer.Name = model.Name; + if (model.Email != null) customer.Email = model.Email; + if (model.Phone != null) customer.Phone = model.Phone; + + customer.UpdatedAt = DateTime.UtcNow; + var result = await repository.UpdateCustomer(id, customer); + + return TypedResults.Ok(new + { + status = "success", + data = result + }); + } + + public static async Task DeleteCustomer(IRepository repository, int id) + { + var entity = await repository.DeleteCustomer(id); + string statusString = "success"; + if (entity == null) statusString = "NotFound"; // return TypedResults.NotFound(new { Error = $"Did not find a customer with id '{id}'." }); + + return TypedResults.Ok(new + { + status = statusString, + data = entity + }); + } + } +} diff --git a/exercise.wwwapi/Enums/Role.cs b/exercise.wwwapi/Enums/Role.cs new file mode 100644 index 0000000..551a617 --- /dev/null +++ b/exercise.wwwapi/Enums/Role.cs @@ -0,0 +1,8 @@ +namespace api_cinema_challenge.Enums +{ + public enum Role + { + Admin, + User + } +} diff --git a/exercise.wwwapi/Models/ApplicationUser.cs b/exercise.wwwapi/Models/ApplicationUser.cs new file mode 100644 index 0000000..6b59b64 --- /dev/null +++ b/exercise.wwwapi/Models/ApplicationUser.cs @@ -0,0 +1,10 @@ +using api_cinema_challenge.Enums; +using Microsoft.AspNetCore.Identity; + +namespace api_cinema_challenge.Models +{ + public class ApplicationUser : IdentityUser + { + public Role Role { get; set; } + } +} diff --git a/exercise.wwwapi/Models/Customer.cs b/exercise.wwwapi/Models/Customer.cs new file mode 100644 index 0000000..04f3f10 --- /dev/null +++ b/exercise.wwwapi/Models/Customer.cs @@ -0,0 +1,17 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace api_cinema_challenge.Models +{ + [Table("customers")] + public class Customer + { + [Key] + public int Id { get; set; } + public string Name { get; set; } + public string Email { get; set; } + public string Phone { get; set; } + public DateTime CreatedAt { get; set; } + public DateTime UpdatedAt { get; set; } + } +} diff --git a/exercise.wwwapi/Program.cs b/exercise.wwwapi/Program.cs index 47f22ef..e8f4dd3 100644 --- a/exercise.wwwapi/Program.cs +++ b/exercise.wwwapi/Program.cs @@ -1,9 +1,106 @@ +using api_cinema_challenge.Data; +using api_cinema_challenge.Endpoints; +using api_cinema_challenge.Models; +using api_cinema_challenge.Repository; +using api_cinema_challenge.Services; +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.AspNetCore.Identity; +using Microsoft.IdentityModel.Tokens; +using Microsoft.OpenApi.Models; +using System.Text; +using System.Text.Json.Serialization; + var builder = WebApplication.CreateBuilder(args); // Add services to the container. -// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle +builder.Services.AddControllers(); builder.Services.AddEndpointsApiExplorer(); -builder.Services.AddSwaggerGen(); + +builder.Services.AddSwaggerGen(option => +{ + option.SwaggerDoc("v1", new OpenApiInfo { Title = "Test API", Version = "v1" }); + option.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme + { + In = ParameterLocation.Header, + Description = "Please enter a valid token", + Name = "Authorization", + Type = SecuritySchemeType.Http, + BearerFormat = "JWT", + Scheme = "Bearer" + }); + option.AddSecurityRequirement(new OpenApiSecurityRequirement + { + { + new OpenApiSecurityScheme + { + Reference = new OpenApiReference + { + Type = ReferenceType.SecurityScheme, + Id = "Bearer" + } + }, + new string[] { } + } + }); +}); + +builder.Services.AddProblemDetails(); +builder.Services.AddApiVersioning(); +builder.Services.AddRouting(options => options.LowercaseUrls = true); +builder.Services.AddDbContext(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); + +// Support string to enum conversions +builder.Services.AddControllers().AddJsonOptions(opt => +{ + opt.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter()); +}); + +// Specify identity requirements +// Must be added before .AddAuthentication otherwise a 404 is thrown on authorized endpoints +builder.Services + .AddIdentity(options => + { + options.SignIn.RequireConfirmedAccount = false; + options.User.RequireUniqueEmail = true; + options.Password.RequireDigit = false; + options.Password.RequiredLength = 6; + options.Password.RequireNonAlphanumeric = false; + options.Password.RequireUppercase = false; + }) + .AddRoles() + .AddEntityFrameworkStores(); + + +// These will eventually be moved to a secrets file, but for alpha development appsettings is fine +var validIssuer = builder.Configuration.GetValue("JwtTokenSettings:ValidIssuer"); +var validAudience = builder.Configuration.GetValue("JwtTokenSettings:ValidAudience"); +var symmetricSecurityKey = builder.Configuration.GetValue("JwtTokenSettings:SymmetricSecurityKey"); + +builder.Services.AddAuthentication(options => +{ + options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; + options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; + options.DefaultScheme = JwtBearerDefaults.AuthenticationScheme; +}) + .AddJwtBearer(options => + { + options.IncludeErrorDetails = true; + options.TokenValidationParameters = new TokenValidationParameters() + { + ClockSkew = TimeSpan.Zero, + ValidateIssuer = true, + ValidateAudience = true, + ValidateLifetime = true, + ValidateIssuerSigningKey = true, + ValidIssuer = validIssuer, + ValidAudience = validAudience, + IssuerSigningKey = new SymmetricSecurityKey( + Encoding.UTF8.GetBytes(symmetricSecurityKey) + ), + }; + }); var app = builder.Build(); @@ -15,6 +112,12 @@ } app.UseHttpsRedirection(); +app.UseStatusCodePages(); + +app.UseAuthentication(); +app.UseAuthorization(); +app.ConfigureCustomerEndpoint(); +app.MapControllers(); app.Run(); \ No newline at end of file diff --git a/exercise.wwwapi/Repository/IRepository.cs b/exercise.wwwapi/Repository/IRepository.cs new file mode 100644 index 0000000..40d39d5 --- /dev/null +++ b/exercise.wwwapi/Repository/IRepository.cs @@ -0,0 +1,14 @@ +using api_cinema_challenge.Models; + +namespace api_cinema_challenge.Repository +{ + public interface IRepository + { + // Customers + Task> GetCustomers(); + Task GetCustomerById(int id); + Task CreateCustomer(Customer customer); + Task UpdateCustomer(int id, Customer customer); + Task DeleteCustomer(int id); + } +} diff --git a/exercise.wwwapi/Repository/Repository.cs b/exercise.wwwapi/Repository/Repository.cs new file mode 100644 index 0000000..53a7871 --- /dev/null +++ b/exercise.wwwapi/Repository/Repository.cs @@ -0,0 +1,50 @@ +using api_cinema_challenge.Models; +using api_cinema_challenge.Data; +using Microsoft.EntityFrameworkCore; + +namespace api_cinema_challenge.Repository +{ + public class Repository : IRepository + { + private CinemaContext _db; + public Repository(CinemaContext db) + { + _db = db; + } + + // Customers + public async Task GetCustomerById(int id) + { + return await _db.Customers.FindAsync(id); + } + + public async Task> GetCustomers() + { + return await _db.Customers.ToListAsync(); + } + + public async Task CreateCustomer(Customer model) + { + await _db.AddAsync(model); + await _db.SaveChangesAsync(); + return model; + } + + public async Task UpdateCustomer(int id, Customer model) + { + var entity = await GetCustomerById(id); + entity = model; + await _db.SaveChangesAsync(); + return entity; + } + + public async Task DeleteCustomer(int id) + { + var entity = await GetCustomerById(id); + if (entity == null) return entity; + _db.Customers.Remove(entity); + await _db.SaveChangesAsync(); + return entity; + } + } +} diff --git a/exercise.wwwapi/Services/TokenService.cs b/exercise.wwwapi/Services/TokenService.cs new file mode 100644 index 0000000..b87ca82 --- /dev/null +++ b/exercise.wwwapi/Services/TokenService.cs @@ -0,0 +1,85 @@ +using api_cinema_challenge.Models; +using Microsoft.IdentityModel.Tokens; +using System.IdentityModel.Tokens.Jwt; +using System.Security.Claims; +using System.Text; +using api_cinema_challenge.Data; +using Microsoft.AspNetCore.Identity; + +namespace api_cinema_challenge.Services +{ + + public class TokenService + { + private const int ExpirationMinutes = 60; + private readonly ILogger _logger; + public TokenService(ILogger logger) + { + _logger = logger; + } + + public string CreateToken(ApplicationUser user) + { + + var expiration = DateTime.UtcNow.AddMinutes(ExpirationMinutes); + var token = CreateJwtToken( + CreateClaims(user), + CreateSigningCredentials(), + expiration + ); + var tokenHandler = new JwtSecurityTokenHandler(); + + _logger.LogInformation("JWT Token created"); + + return tokenHandler.WriteToken(token); + } + + private JwtSecurityToken CreateJwtToken(List claims, SigningCredentials credentials, + DateTime expiration) => + new( + new ConfigurationBuilder().AddJsonFile("appsettings.json").Build().GetSection("JwtTokenSettings")["ValidIssuer"], + new ConfigurationBuilder().AddJsonFile("appsettings.json").Build().GetSection("JwtTokenSettings")["ValidAudience"], + claims, + expires: expiration, + signingCredentials: credentials + ); + + private List CreateClaims(ApplicationUser user) + { + //var jwtSub = new ConfigurationBuilder().AddJsonFile("appsettings.json").Build().GetSection("JwtTokenSettings")["JwtRegisteredClaimNamesSub"]; + + try + { + var claims = new List + { + new Claim(JwtRegisteredClaimNames.Sub, user.Id), + new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()), + new Claim(JwtRegisteredClaimNames.Iat, DateTimeOffset.UtcNow.ToUnixTimeSeconds().ToString()), + new Claim(ClaimTypes.NameIdentifier, user.Id), + new Claim(ClaimTypes.Name, user.UserName), + new Claim(ClaimTypes.Email, user.Email), + new Claim(ClaimTypes.Role, user.Role.ToString()) + }; + + return claims; + } + catch (Exception e) + { + Console.WriteLine(e); + throw; + } + } + + private SigningCredentials CreateSigningCredentials() + { + var symmetricSecurityKey = new ConfigurationBuilder().AddJsonFile("appsettings.json").Build().GetSection("JwtTokenSettings")["SymmetricSecurityKey"]; + + return new SigningCredentials( + new SymmetricSecurityKey( + Encoding.UTF8.GetBytes(symmetricSecurityKey) + ), + SecurityAlgorithms.HmacSha256 + ); + } + } +} diff --git a/exercise.wwwapi/exercise.wwwapi.csproj b/exercise.wwwapi/exercise.wwwapi.csproj index 56929a8..655ff0c 100644 --- a/exercise.wwwapi/exercise.wwwapi.csproj +++ b/exercise.wwwapi/exercise.wwwapi.csproj @@ -1,4 +1,4 @@ - + net9.0 @@ -8,7 +8,19 @@ - + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + +