Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 52 additions & 19 deletions Api/Api.csproj
Original file line number Diff line number Diff line change
@@ -1,25 +1,58 @@
<Project Sdk="Microsoft.NET.Sdk.Web">

<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<UserSecretsId>60e7b3a2-6cbf-4971-88b0-7b898d2506bf</UserSecretsId>
</PropertyGroup>
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<UserSecretsId>60e7b3a2-6cbf-4971-88b0-7b898d2506bf</UserSecretsId>
<SpaRoot>../ClientApp/</SpaRoot>
<SpaProxyServerUrl>http://localhost:5173</SpaProxyServerUrl>
<SpaProxyLaunchCommand>npm run dev</SpaProxyLaunchCommand>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="10.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="10.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="10.0.3" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.6.2" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="10.0.5" />
<PackageReference Include="Microsoft.AspNetCore.SpaProxy" Version="10.0.5" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="10.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="10.0.3" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.6.2" />
</ItemGroup>

<ItemGroup>
<Compile Remove="Tests/**/*.cs" />
</ItemGroup>
<ItemGroup>
<Compile Remove="Tests/**/*.cs" />
</ItemGroup>

<ItemGroup>
<Folder Include="bin\Debug\net10.0\BuildHost-net472\cs\" />
<Folder Include="bin\Debug\net10.0\BuildHost-net472\de\" />
<Folder Include="bin\Debug\net10.0\BuildHost-net472\es\" />
<Folder Include="bin\Debug\net10.0\BuildHost-net472\fr\" />
<Folder Include="bin\Debug\net10.0\BuildHost-net472\it\" />
<Folder Include="bin\Debug\net10.0\BuildHost-net472\ja\" />
<Folder Include="bin\Debug\net10.0\BuildHost-net472\ko\" />
<Folder Include="bin\Debug\net10.0\BuildHost-net472\pl\" />
<Folder Include="bin\Debug\net10.0\BuildHost-net472\pt-BR\" />
<Folder Include="bin\Debug\net10.0\BuildHost-net472\ru\" />
<Folder Include="bin\Debug\net10.0\BuildHost-net472\tr\" />
<Folder Include="bin\Debug\net10.0\BuildHost-net472\zh-Hans\" />
<Folder Include="bin\Debug\net10.0\BuildHost-net472\zh-Hant\" />
<Folder Include="bin\Debug\net10.0\BuildHost-netcore\cs\" />
<Folder Include="bin\Debug\net10.0\BuildHost-netcore\de\" />
<Folder Include="bin\Debug\net10.0\BuildHost-netcore\es\" />
<Folder Include="bin\Debug\net10.0\BuildHost-netcore\fr\" />
<Folder Include="bin\Debug\net10.0\BuildHost-netcore\it\" />
<Folder Include="bin\Debug\net10.0\BuildHost-netcore\ja\" />
<Folder Include="bin\Debug\net10.0\BuildHost-netcore\ko\" />
<Folder Include="bin\Debug\net10.0\BuildHost-netcore\pl\" />
<Folder Include="bin\Debug\net10.0\BuildHost-netcore\pt-BR\" />
<Folder Include="bin\Debug\net10.0\BuildHost-netcore\ru\" />
<Folder Include="bin\Debug\net10.0\BuildHost-netcore\tr\" />
<Folder Include="bin\Debug\net10.0\BuildHost-netcore\zh-Hans\" />
<Folder Include="bin\Debug\net10.0\BuildHost-netcore\zh-Hant\" />
</ItemGroup>

</Project>
6 changes: 3 additions & 3 deletions Api/Contracts/AppUserDTO.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,6 @@ public sealed record AppUserDto(
string Role,
DateTime CreatedAtUtc)
{
public static AppUserDto FromModel(AppUser user) =>
new(user.Id, user.Username, user.Role, user.CreatedAtUtc);
}
public static AppUserDto FromModel(ApplicationUser user) =>
new(user.Id, user.UserName ?? string.Empty, user.Role, user.CreatedAtUtc);
}
80 changes: 6 additions & 74 deletions Api/Controllers/AuthController.cs
Original file line number Diff line number Diff line change
@@ -1,86 +1,18 @@
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text;
using Api.Contracts;
using Api.Data;
using Api.Models;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Microsoft.IdentityModel.Tokens;

namespace Api.Controllers;

[ApiController]
[Route("api/[controller]")]
public class AuthController(
IConfiguration configuration,
AppDbContext dbContext,
IPasswordHasher<AppUser> passwordHasher) : ControllerBase
public class AuthController : Controller
{
[HttpPost("token")]
[AllowAnonymous]
public async Task<ActionResult<TokenResponse>> Token([FromBody] TokenRequest request)
[Route("api/logout")]
public async Task<ActionResult> Logout(SignInManager<ApplicationUser> signInManager)
{
if (string.IsNullOrWhiteSpace(request.Username) || string.IsNullOrWhiteSpace(request.Password))
{
return BadRequest("Username and password are required.");
}
await signInManager.SignOutAsync();

var normalizedUsername = request.Username.Trim().ToUpperInvariant();
var user = await dbContext.Users.FirstOrDefaultAsync(candidate =>
candidate.NormalizedUsername == normalizedUsername);

if (user is null)
{
return Unauthorized();
}

var passwordVerificationResult = passwordHasher.VerifyHashedPassword(user, user.PasswordHash, request.Password);
if (passwordVerificationResult == PasswordVerificationResult.Failed)
{
return Unauthorized();
}

var issuer = configuration["Jwt:Issuer"];
var audience = configuration["Jwt:Audience"];
var signingKey = configuration["Jwt:SigningKey"];

if (string.IsNullOrWhiteSpace(issuer) || string.IsNullOrWhiteSpace(audience) ||
string.IsNullOrWhiteSpace(signingKey))
{
return StatusCode(StatusCodes.Status500InternalServerError, "JWT configuration is missing.");
}

var expiresAt = DateTime.UtcNow.AddHours(1);
var claims = new[]
{
new Claim(JwtRegisteredClaimNames.Sub, user.Id.ToString()),
new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
new Claim(ClaimTypes.NameIdentifier, user.Id.ToString()),
new Claim(ClaimTypes.Name, user.Username),
new Claim(ClaimTypes.Role, user.Role)
};

var credentials = new SigningCredentials(
new SymmetricSecurityKey(Encoding.UTF8.GetBytes(signingKey)),
SecurityAlgorithms.HmacSha256);

var token = new JwtSecurityToken(
issuer: issuer,
audience: audience,
claims: claims,
expires: expiresAt,
signingCredentials: credentials);

var tokenString = new JwtSecurityTokenHandler().WriteToken(token);

var userDto = AppUserDto.FromModel(user);
return Ok(new TokenResponse(tokenString, expiresAt, userDto));
return Ok();
}
}

public sealed record TokenRequest(string Username, string Password);

public sealed record TokenResponse(string AccessToken, DateTime ExpiresAtUtc, AppUserDto User);
}
25 changes: 25 additions & 0 deletions Api/Controllers/MeController.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
using Api.Contracts;
using Api.Models;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;

namespace Api.Controllers;

[ApiController]
[Route("api/[controller]")]
public class MeController : Controller
{
public async Task<ActionResult<AppUserDto>> Index(UserManager<ApplicationUser> userManager)
{
var user = await userManager.GetUserAsync(User);

if (user == null)
{
return StatusCode(StatusCodes.Status403Forbidden);
}

var userDto = AppUserDto.FromModel(user);

return Ok(userDto);
}
}
21 changes: 11 additions & 10 deletions Api/Data/AppDbContext.cs
Original file line number Diff line number Diff line change
@@ -1,22 +1,23 @@
using Api.Models;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;

namespace Api.Data;

public sealed class AppDbContext(DbContextOptions<AppDbContext> options) : DbContext(options)
public sealed class AppDbContext(DbContextOptions<AppDbContext> options)
: IdentityDbContext<ApplicationUser, IdentityRole<Guid>, Guid>(options)
{
public DbSet<AppUser> Users => Set<AppUser>();

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<AppUser>(entity =>
base.OnModelCreating(modelBuilder);

modelBuilder.Entity<ApplicationUser>(entity =>
{
entity.HasKey(user => user.Id);
entity.Property(user => user.Username).HasMaxLength(100).IsRequired();
entity.Property(user => user.NormalizedUsername).HasMaxLength(100).IsRequired();
entity.Property(user => user.PasswordHash).IsRequired();
entity.Property(user => user.Role).HasMaxLength(50).IsRequired();
entity.HasIndex(user => user.NormalizedUsername).IsUnique();
entity.Property(user => user.CreatedAtUtc).IsRequired();
entity.Property(user => user.Email).IsRequired();
entity.Property(user => user.NormalizedEmail).IsRequired();
entity.Property(user => user.NormalizedUserName).IsRequired();
});
}
}
18 changes: 11 additions & 7 deletions Api/Data/DbSeeder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,28 +15,32 @@ public static class DbSeeder
public static async Task seedAsync(IServiceProvider services, IConfiguration configuration)
{
var demoUsername = "test";
var demoPassword = "dev";
var demoEmail = "test@example.com";
var demoPassword = "Wertyd456!";

if (string.IsNullOrWhiteSpace(demoUsername) || string.IsNullOrWhiteSpace(demoPassword))
{
return;
}

var dbContext = services.GetRequiredService<AppDbContext>();
var passwordHasher = services.GetRequiredService<IPasswordHasher<AppUser>>();
var passwordHasher = services.GetRequiredService<IPasswordHasher<ApplicationUser>>();

var normalizedUsername = demoUsername.Trim().ToUpperInvariant();
var existingUser = await dbContext.Users.FirstOrDefaultAsync(user => user.NormalizedUsername == normalizedUsername);
var existingUser =
await dbContext.Users.FirstOrDefaultAsync(user => user.NormalizedUserName == normalizedUsername);

if (existingUser is not null)
{
return;
}

var user = new AppUser
var user = new ApplicationUser
{
Username = demoUsername.Trim(),
NormalizedUsername = normalizedUsername,
UserName = demoUsername.Trim(),
NormalizedUserName = normalizedUsername,
Email = demoEmail,
NormalizedEmail = demoEmail.Trim().ToUpperInvariant(),
PasswordHash = string.Empty,
Role = "Admin"
};
Expand All @@ -46,4 +50,4 @@ public static async Task seedAsync(IServiceProvider services, IConfiguration con
dbContext.Users.Add(user);
await dbContext.SaveChangesAsync();
}
}
}
Loading