mirror of
https://github.com/Kareadita/Kavita.git
synced 2025-07-09 03:04:19 -04:00
Merge pull request #9 from Kareadita/feature/user-login
User Login + Member fetching
This commit is contained in:
commit
497496c609
5
.gitignore
vendored
5
.gitignore
vendored
@ -443,5 +443,6 @@ $RECYCLE.BIN/
|
|||||||
|
|
||||||
# App specific
|
# App specific
|
||||||
appsettings.json
|
appsettings.json
|
||||||
/API/datingapp.db-shm
|
/API/kavita.db
|
||||||
/API/datingapp.db-wal
|
/API/kavita.db-shm
|
||||||
|
/API/kavita.db-wal
|
@ -5,6 +5,7 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
<PackageReference Include="AutoMapper.Extensions.Microsoft.DependencyInjection" Version="8.1.0" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="5.0.1" NoWarn="NU1605" />
|
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="5.0.1" NoWarn="NU1605" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="5.0.1" NoWarn="NU1605" />
|
<PackageReference Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="5.0.1" NoWarn="NU1605" />
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="5.0.1">
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="5.0.1">
|
||||||
|
94
API/Controllers/AccountController.cs
Normal file
94
API/Controllers/AccountController.cs
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
using System;
|
||||||
|
using System.Security.Cryptography;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using API.Data;
|
||||||
|
using API.DTOs;
|
||||||
|
using API.Entities;
|
||||||
|
using API.Interfaces;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
|
namespace API.Controllers
|
||||||
|
{
|
||||||
|
public class AccountController : BaseApiController
|
||||||
|
{
|
||||||
|
private readonly DataContext _context;
|
||||||
|
private readonly ITokenService _tokenService;
|
||||||
|
private readonly IUserRepository _userRepository;
|
||||||
|
private readonly ILogger<AccountController> _logger;
|
||||||
|
|
||||||
|
public AccountController(DataContext context, ITokenService tokenService, IUserRepository userRepository, ILogger<AccountController> logger)
|
||||||
|
{
|
||||||
|
_context = context;
|
||||||
|
_tokenService = tokenService;
|
||||||
|
_userRepository = userRepository;
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost("register")]
|
||||||
|
public async Task<ActionResult<UserDto>> Register(RegisterDto registerDto)
|
||||||
|
{
|
||||||
|
_logger.LogInformation("Username: " + registerDto.Password);
|
||||||
|
if (await UserExists(registerDto.Username))
|
||||||
|
{
|
||||||
|
return BadRequest("Username is taken.");
|
||||||
|
}
|
||||||
|
|
||||||
|
using var hmac = new HMACSHA512();
|
||||||
|
var user = new AppUser
|
||||||
|
{
|
||||||
|
UserName = registerDto.Username.ToLower(),
|
||||||
|
PasswordHash = hmac.ComputeHash(Encoding.UTF8.GetBytes(registerDto.Password)),
|
||||||
|
PasswordSalt = hmac.Key,
|
||||||
|
IsAdmin = registerDto.IsAdmin,
|
||||||
|
LastActive = DateTime.Now
|
||||||
|
};
|
||||||
|
|
||||||
|
_context.Users.Add(user);
|
||||||
|
await _context.SaveChangesAsync();
|
||||||
|
|
||||||
|
return new UserDto()
|
||||||
|
{
|
||||||
|
Username = user.UserName,
|
||||||
|
Token = _tokenService.CreateToken(user),
|
||||||
|
IsAdmin = user.IsAdmin
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost("login")]
|
||||||
|
public async Task<ActionResult<UserDto>> Login(LoginDto loginDto)
|
||||||
|
{
|
||||||
|
var user = await _userRepository.GetUserByUsernameAsync(loginDto.Username);
|
||||||
|
|
||||||
|
if (user == null) return Unauthorized("Invalid username");
|
||||||
|
|
||||||
|
using var hmac = new HMACSHA512(user.PasswordSalt);
|
||||||
|
|
||||||
|
var computedHash = hmac.ComputeHash(Encoding.UTF8.GetBytes(loginDto.Password));
|
||||||
|
|
||||||
|
for (int i = 0; i < computedHash.Length; i++)
|
||||||
|
{
|
||||||
|
if (computedHash[i] != user.PasswordHash[i]) return Unauthorized("Invalid password");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update LastActive on account
|
||||||
|
user.LastActive = DateTime.Now;
|
||||||
|
_userRepository.Update(user);
|
||||||
|
await _userRepository.SaveAllAsync();
|
||||||
|
|
||||||
|
return new UserDto()
|
||||||
|
{
|
||||||
|
Username = user.UserName,
|
||||||
|
Token = _tokenService.CreateToken(user),
|
||||||
|
IsAdmin = user.IsAdmin
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<bool> UserExists(string username)
|
||||||
|
{
|
||||||
|
return await _context.Users.AnyAsync(user => user.UserName == username.ToLower());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
10
API/Controllers/BaseApiController.cs
Normal file
10
API/Controllers/BaseApiController.cs
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
|
namespace API.Controllers
|
||||||
|
{
|
||||||
|
[ApiController]
|
||||||
|
[Route("api/[controller]")]
|
||||||
|
public class BaseApiController : ControllerBase
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
28
API/Controllers/UsersController.cs
Normal file
28
API/Controllers/UsersController.cs
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
using System.Collections;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using API.Data;
|
||||||
|
using API.DTOs;
|
||||||
|
using API.Interfaces;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
|
namespace API.Controllers
|
||||||
|
{
|
||||||
|
public class UsersController : BaseApiController
|
||||||
|
{
|
||||||
|
private readonly DataContext _context;
|
||||||
|
private readonly IUserRepository _userRepository;
|
||||||
|
|
||||||
|
public UsersController(DataContext context, IUserRepository userRepository)
|
||||||
|
{
|
||||||
|
_context = context;
|
||||||
|
_userRepository = userRepository;
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet]
|
||||||
|
public async Task<ActionResult<IEnumerable<MemberDto>>> GetUsers()
|
||||||
|
{
|
||||||
|
return Ok(await _userRepository.GetMembersAsync());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
8
API/DTOs/LoginDto.cs
Normal file
8
API/DTOs/LoginDto.cs
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
namespace API.DTOs
|
||||||
|
{
|
||||||
|
public class LoginDto
|
||||||
|
{
|
||||||
|
public string Username { get; set; }
|
||||||
|
public string Password { get; set; }
|
||||||
|
}
|
||||||
|
}
|
16
API/DTOs/MemberDto.cs
Normal file
16
API/DTOs/MemberDto.cs
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace API.DTOs
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Represents a member of a Kavita server.
|
||||||
|
/// </summary>
|
||||||
|
public class MemberDto
|
||||||
|
{
|
||||||
|
public int Id { get; set; }
|
||||||
|
public string Username { get; set; }
|
||||||
|
public DateTime Created { get; set; }
|
||||||
|
public DateTime LastActive { get; set; }
|
||||||
|
public bool IsAdmin { get; set; }
|
||||||
|
}
|
||||||
|
}
|
14
API/DTOs/RegisterDto.cs
Normal file
14
API/DTOs/RegisterDto.cs
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
|
||||||
|
namespace API.DTOs
|
||||||
|
{
|
||||||
|
public class RegisterDto
|
||||||
|
{
|
||||||
|
[Required]
|
||||||
|
public string Username { get; set; }
|
||||||
|
[Required]
|
||||||
|
[StringLength(8, MinimumLength = 4)]
|
||||||
|
public string Password { get; set; }
|
||||||
|
public bool IsAdmin { get; set; }
|
||||||
|
}
|
||||||
|
}
|
9
API/DTOs/UserDto.cs
Normal file
9
API/DTOs/UserDto.cs
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
namespace API.DTOs
|
||||||
|
{
|
||||||
|
public class UserDto
|
||||||
|
{
|
||||||
|
public string Username { get; set; }
|
||||||
|
public string Token { get; set; }
|
||||||
|
public bool IsAdmin { get; set; }
|
||||||
|
}
|
||||||
|
}
|
15
API/Data/DataContext.cs
Normal file
15
API/Data/DataContext.cs
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
using API.Entities;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
|
namespace API.Data
|
||||||
|
{
|
||||||
|
public class DataContext : DbContext
|
||||||
|
{
|
||||||
|
public DataContext(DbContextOptions options) : base(options)
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public DbSet<AppUser> Users { get; set; }
|
||||||
|
}
|
||||||
|
}
|
56
API/Data/Migrations/20201213205325_AddUser.Designer.cs
generated
Normal file
56
API/Data/Migrations/20201213205325_AddUser.Designer.cs
generated
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
// <auto-generated />
|
||||||
|
using System;
|
||||||
|
using API.Data;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||||
|
|
||||||
|
namespace API.Data.Migrations
|
||||||
|
{
|
||||||
|
[DbContext(typeof(DataContext))]
|
||||||
|
[Migration("20201213205325_AddUser")]
|
||||||
|
partial class AddUser
|
||||||
|
{
|
||||||
|
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||||
|
{
|
||||||
|
#pragma warning disable 612, 618
|
||||||
|
modelBuilder
|
||||||
|
.HasAnnotation("ProductVersion", "5.0.1");
|
||||||
|
|
||||||
|
modelBuilder.Entity("API.Entities.AppUser", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<DateTime>("Created")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<bool>("IsAdmin")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<DateTime>("LastActive")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<byte[]>("PasswordHash")
|
||||||
|
.HasColumnType("BLOB");
|
||||||
|
|
||||||
|
b.Property<byte[]>("PasswordSalt")
|
||||||
|
.HasColumnType("BLOB");
|
||||||
|
|
||||||
|
b.Property<uint>("RowVersion")
|
||||||
|
.IsConcurrencyToken()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("UserName")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.ToTable("Users");
|
||||||
|
});
|
||||||
|
#pragma warning restore 612, 618
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
36
API/Data/Migrations/20201213205325_AddUser.cs
Normal file
36
API/Data/Migrations/20201213205325_AddUser.cs
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
namespace API.Data.Migrations
|
||||||
|
{
|
||||||
|
public partial class AddUser : Migration
|
||||||
|
{
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "Users",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<int>(type: "INTEGER", nullable: false)
|
||||||
|
.Annotation("Sqlite:Autoincrement", true),
|
||||||
|
UserName = table.Column<string>(type: "TEXT", nullable: true),
|
||||||
|
PasswordHash = table.Column<byte[]>(type: "BLOB", nullable: true),
|
||||||
|
PasswordSalt = table.Column<byte[]>(type: "BLOB", nullable: true),
|
||||||
|
Created = table.Column<DateTime>(type: "TEXT", nullable: false),
|
||||||
|
LastActive = table.Column<DateTime>(type: "TEXT", nullable: false),
|
||||||
|
IsAdmin = table.Column<bool>(type: "INTEGER", nullable: false),
|
||||||
|
RowVersion = table.Column<uint>(type: "INTEGER", nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_Users", x => x.Id);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "Users");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
54
API/Data/Migrations/DataContextModelSnapshot.cs
Normal file
54
API/Data/Migrations/DataContextModelSnapshot.cs
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
// <auto-generated />
|
||||||
|
using System;
|
||||||
|
using API.Data;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||||
|
|
||||||
|
namespace API.Data.Migrations
|
||||||
|
{
|
||||||
|
[DbContext(typeof(DataContext))]
|
||||||
|
partial class DataContextModelSnapshot : ModelSnapshot
|
||||||
|
{
|
||||||
|
protected override void BuildModel(ModelBuilder modelBuilder)
|
||||||
|
{
|
||||||
|
#pragma warning disable 612, 618
|
||||||
|
modelBuilder
|
||||||
|
.HasAnnotation("ProductVersion", "5.0.1");
|
||||||
|
|
||||||
|
modelBuilder.Entity("API.Entities.AppUser", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<DateTime>("Created")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<bool>("IsAdmin")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<DateTime>("LastActive")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<byte[]>("PasswordHash")
|
||||||
|
.HasColumnType("BLOB");
|
||||||
|
|
||||||
|
b.Property<byte[]>("PasswordSalt")
|
||||||
|
.HasColumnType("BLOB");
|
||||||
|
|
||||||
|
b.Property<uint>("RowVersion")
|
||||||
|
.IsConcurrencyToken()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("UserName")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.ToTable("Users");
|
||||||
|
});
|
||||||
|
#pragma warning restore 612, 618
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
61
API/Data/UserRepository.cs
Normal file
61
API/Data/UserRepository.cs
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using API.DTOs;
|
||||||
|
using API.Entities;
|
||||||
|
using API.Interfaces;
|
||||||
|
using AutoMapper;
|
||||||
|
using AutoMapper.QueryableExtensions;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
|
namespace API.Data
|
||||||
|
{
|
||||||
|
public class UserRepository : IUserRepository
|
||||||
|
{
|
||||||
|
private readonly DataContext _context;
|
||||||
|
private readonly IMapper _mapper;
|
||||||
|
|
||||||
|
public UserRepository(DataContext context, IMapper mapper)
|
||||||
|
{
|
||||||
|
_context = context;
|
||||||
|
_mapper = mapper;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Update(AppUser user)
|
||||||
|
{
|
||||||
|
_context.Entry(user).State = EntityState.Modified;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> SaveAllAsync()
|
||||||
|
{
|
||||||
|
return await _context.SaveChangesAsync() > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<IEnumerable<AppUser>> GetUsersAsync()
|
||||||
|
{
|
||||||
|
return await _context.Users.ToListAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<AppUser> GetUserByIdAsync(int id)
|
||||||
|
{
|
||||||
|
return await _context.Users.FindAsync(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<AppUser> GetUserByUsernameAsync(string username)
|
||||||
|
{
|
||||||
|
return await _context.Users.SingleOrDefaultAsync(x => x.UserName == username);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<IEnumerable<MemberDto>> GetMembersAsync()
|
||||||
|
{
|
||||||
|
return await _context.Users.ProjectTo<MemberDto>(_mapper.ConfigurationProvider).ToListAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<MemberDto> GetMemberAsync(string username)
|
||||||
|
{
|
||||||
|
return await _context.Users.Where(x => x.UserName == username)
|
||||||
|
.ProjectTo<MemberDto>(_mapper.ConfigurationProvider)
|
||||||
|
.SingleOrDefaultAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
27
API/Entities/AppUser.cs
Normal file
27
API/Entities/AppUser.cs
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
using System;
|
||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
using API.Entities.Interfaces;
|
||||||
|
|
||||||
|
|
||||||
|
namespace API.Entities
|
||||||
|
{
|
||||||
|
public class AppUser : IHasConcurrencyToken
|
||||||
|
{
|
||||||
|
public int Id { get; set; }
|
||||||
|
public string UserName { get; set; }
|
||||||
|
public byte[] PasswordHash { get; set; }
|
||||||
|
public byte[] PasswordSalt { get; set; }
|
||||||
|
public DateTime Created { get; set; } = DateTime.Now;
|
||||||
|
public DateTime LastActive { get; set; }
|
||||||
|
public bool IsAdmin { get; set; }
|
||||||
|
|
||||||
|
[ConcurrencyCheck]
|
||||||
|
public uint RowVersion { get; set; }
|
||||||
|
|
||||||
|
public void OnSavingChanges()
|
||||||
|
{
|
||||||
|
RowVersion++;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
19
API/Entities/Interfaces/IHasConcurrencyToken.cs
Normal file
19
API/Entities/Interfaces/IHasConcurrencyToken.cs
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
namespace API.Entities.Interfaces
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// An interface abstracting an entity that has a concurrency token.
|
||||||
|
/// </summary>
|
||||||
|
public interface IHasConcurrencyToken
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the version of this row. Acts as a concurrency token.
|
||||||
|
/// </summary>
|
||||||
|
uint RowVersion { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Called when saving changes to this entity.
|
||||||
|
/// </summary>
|
||||||
|
void OnSavingChanges();
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
16
API/Errors/ApiException.cs
Normal file
16
API/Errors/ApiException.cs
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
namespace API.Errors
|
||||||
|
{
|
||||||
|
public class ApiException
|
||||||
|
{
|
||||||
|
public int Status { get; set; }
|
||||||
|
public string Message { get; set; }
|
||||||
|
public string Details { get; set; }
|
||||||
|
|
||||||
|
public ApiException(int status, string message = null, string details = null)
|
||||||
|
{
|
||||||
|
Status = status;
|
||||||
|
Message = message;
|
||||||
|
Details = details;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
27
API/Extensions/ApplicationServiceExtensions.cs
Normal file
27
API/Extensions/ApplicationServiceExtensions.cs
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
using API.Data;
|
||||||
|
using API.Helpers;
|
||||||
|
using API.Interfaces;
|
||||||
|
using API.Services;
|
||||||
|
using AutoMapper;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.Extensions.Configuration;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
|
||||||
|
namespace API.Extensions
|
||||||
|
{
|
||||||
|
public static class ApplicationServiceExtensions
|
||||||
|
{
|
||||||
|
public static IServiceCollection AddApplicationServices(this IServiceCollection services, IConfiguration config)
|
||||||
|
{
|
||||||
|
services.AddAutoMapper(typeof(AutoMapperProfiles).Assembly);
|
||||||
|
services.AddScoped<IUserRepository, UserRepository>();
|
||||||
|
services.AddScoped<ITokenService, TokenService>();
|
||||||
|
services.AddDbContext<DataContext>(options =>
|
||||||
|
{
|
||||||
|
options.UseSqlite(config.GetConnectionString("DefaultConnection"));
|
||||||
|
});
|
||||||
|
|
||||||
|
return services;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
12
API/Extensions/ClaimsPrincipalExtensions.cs
Normal file
12
API/Extensions/ClaimsPrincipalExtensions.cs
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
using System.Security.Claims;
|
||||||
|
|
||||||
|
namespace API.Extensions
|
||||||
|
{
|
||||||
|
public static class ClaimsPrincipalExtensions
|
||||||
|
{
|
||||||
|
public static string GetUsername(this ClaimsPrincipal user)
|
||||||
|
{
|
||||||
|
return user.FindFirst(ClaimTypes.NameIdentifier)?.Value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
28
API/Extensions/IdentityServiceExtensions.cs
Normal file
28
API/Extensions/IdentityServiceExtensions.cs
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
using System.Collections;
|
||||||
|
using System.Text;
|
||||||
|
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
||||||
|
using Microsoft.Extensions.Configuration;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Microsoft.IdentityModel.Tokens;
|
||||||
|
|
||||||
|
namespace API.Extensions
|
||||||
|
{
|
||||||
|
public static class IdentityServiceExtensions
|
||||||
|
{
|
||||||
|
public static IServiceCollection AddIdentityServices(this IServiceCollection services, IConfiguration config)
|
||||||
|
{
|
||||||
|
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
|
||||||
|
.AddJwtBearer(options =>
|
||||||
|
{
|
||||||
|
options.TokenValidationParameters = new TokenValidationParameters()
|
||||||
|
{
|
||||||
|
ValidateIssuerSigningKey = true,
|
||||||
|
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(config["TokenKey"])),
|
||||||
|
ValidateIssuer = false,
|
||||||
|
ValidateAudience = false
|
||||||
|
};
|
||||||
|
});
|
||||||
|
return services;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
14
API/Helpers/AutoMapperProfiles.cs
Normal file
14
API/Helpers/AutoMapperProfiles.cs
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
using API.DTOs;
|
||||||
|
using API.Entities;
|
||||||
|
using AutoMapper;
|
||||||
|
|
||||||
|
namespace API.Helpers
|
||||||
|
{
|
||||||
|
public class AutoMapperProfiles : Profile
|
||||||
|
{
|
||||||
|
public AutoMapperProfiles()
|
||||||
|
{
|
||||||
|
CreateMap<AppUser, MemberDto>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
9
API/Interfaces/ITokenService.cs
Normal file
9
API/Interfaces/ITokenService.cs
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
using API.Entities;
|
||||||
|
|
||||||
|
namespace API.Interfaces
|
||||||
|
{
|
||||||
|
public interface ITokenService
|
||||||
|
{
|
||||||
|
string CreateToken(AppUser user);
|
||||||
|
}
|
||||||
|
}
|
19
API/Interfaces/IUserRepository.cs
Normal file
19
API/Interfaces/IUserRepository.cs
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
using System.Collections;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using API.DTOs;
|
||||||
|
using API.Entities;
|
||||||
|
|
||||||
|
namespace API.Interfaces
|
||||||
|
{
|
||||||
|
public interface IUserRepository
|
||||||
|
{
|
||||||
|
void Update(AppUser user);
|
||||||
|
Task<bool> SaveAllAsync();
|
||||||
|
Task<IEnumerable<AppUser>> GetUsersAsync();
|
||||||
|
Task<AppUser> GetUserByIdAsync(int id);
|
||||||
|
Task<AppUser> GetUserByUsernameAsync(string username);
|
||||||
|
Task<IEnumerable<MemberDto>> GetMembersAsync();
|
||||||
|
Task<MemberDto> GetMemberAsync(string username);
|
||||||
|
}
|
||||||
|
}
|
56
API/Middleware/ExceptionMiddleware.cs
Normal file
56
API/Middleware/ExceptionMiddleware.cs
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
using System;
|
||||||
|
using System.Net;
|
||||||
|
using System.Text.Json;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using API.Errors;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
using Microsoft.Extensions.Hosting;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
|
namespace API.Middleware
|
||||||
|
{
|
||||||
|
public class ExceptionMiddleware
|
||||||
|
{
|
||||||
|
private readonly RequestDelegate _next;
|
||||||
|
private readonly ILogger<ExceptionMiddleware> _logger;
|
||||||
|
private readonly IHostEnvironment _env;
|
||||||
|
|
||||||
|
|
||||||
|
public ExceptionMiddleware(RequestDelegate next, ILogger<ExceptionMiddleware> logger, IHostEnvironment env)
|
||||||
|
{
|
||||||
|
_next = next;
|
||||||
|
_logger = logger;
|
||||||
|
_env = env;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task InvokeAsync(HttpContext context)
|
||||||
|
{
|
||||||
|
_logger.LogError("The middleware called");
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await _next(context); // downstream middlewares or http call
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, ex.Message);
|
||||||
|
context.Response.ContentType = "application/json";
|
||||||
|
context.Response.StatusCode = (int) HttpStatusCode.InternalServerError;
|
||||||
|
|
||||||
|
var response = _env.IsDevelopment()
|
||||||
|
? new ApiException(context.Response.StatusCode, ex.Message, ex.StackTrace)
|
||||||
|
: new ApiException(context.Response.StatusCode, "Internal Server Error");
|
||||||
|
|
||||||
|
var options = new JsonSerializerOptions
|
||||||
|
{
|
||||||
|
PropertyNamingPolicy =
|
||||||
|
JsonNamingPolicy.CamelCase
|
||||||
|
};
|
||||||
|
|
||||||
|
var json = JsonSerializer.Serialize(response, options);
|
||||||
|
|
||||||
|
await context.Response.WriteAsync(json);
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,9 +1,9 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using API.Data;
|
||||||
using Microsoft.AspNetCore.Hosting;
|
using Microsoft.AspNetCore.Hosting;
|
||||||
using Microsoft.Extensions.Configuration;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Microsoft.Extensions.Hosting;
|
using Microsoft.Extensions.Hosting;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
@ -11,9 +11,26 @@ namespace API
|
|||||||
{
|
{
|
||||||
public class Program
|
public class Program
|
||||||
{
|
{
|
||||||
public static void Main(string[] args)
|
public static async Task Main(string[] args)
|
||||||
{
|
{
|
||||||
CreateHostBuilder(args).Build().Run();
|
var host = CreateHostBuilder(args).Build();
|
||||||
|
|
||||||
|
using var scope = host.Services.CreateScope();
|
||||||
|
var services = scope.ServiceProvider;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var context = services.GetRequiredService<DataContext>();
|
||||||
|
// Apply all migrations on startup
|
||||||
|
await context.Database.MigrateAsync();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
var logger = services.GetRequiredService < ILogger<Program>>();
|
||||||
|
logger.LogError(ex, "An error occurred during migration");
|
||||||
|
}
|
||||||
|
|
||||||
|
await host.RunAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static IHostBuilder CreateHostBuilder(string[] args) =>
|
public static IHostBuilder CreateHostBuilder(string[] args) =>
|
||||||
|
@ -20,7 +20,7 @@
|
|||||||
"API": {
|
"API": {
|
||||||
"commandName": "Project",
|
"commandName": "Project",
|
||||||
"dotnetRunMessages": "true",
|
"dotnetRunMessages": "true",
|
||||||
"launchBrowser": true,
|
"launchBrowser": false,
|
||||||
"launchUrl": "swagger",
|
"launchUrl": "swagger",
|
||||||
"applicationUrl": "https://localhost:5001;http://localhost:5000",
|
"applicationUrl": "https://localhost:5001;http://localhost:5000",
|
||||||
"environmentVariables": {
|
"environmentVariables": {
|
||||||
|
46
API/Services/TokenService.cs
Normal file
46
API/Services/TokenService.cs
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IdentityModel.Tokens.Jwt;
|
||||||
|
using System.Security.Claims;
|
||||||
|
using System.Text;
|
||||||
|
using API.Entities;
|
||||||
|
using API.Interfaces;
|
||||||
|
using Microsoft.Extensions.Configuration;
|
||||||
|
using Microsoft.IdentityModel.Tokens;
|
||||||
|
using JwtRegisteredClaimNames = Microsoft.IdentityModel.JsonWebTokens.JwtRegisteredClaimNames;
|
||||||
|
|
||||||
|
|
||||||
|
namespace API.Services
|
||||||
|
{
|
||||||
|
public class TokenService : ITokenService
|
||||||
|
{
|
||||||
|
private readonly SymmetricSecurityKey _key;
|
||||||
|
|
||||||
|
public TokenService(IConfiguration config)
|
||||||
|
{
|
||||||
|
_key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(config["TokenKey"]));
|
||||||
|
}
|
||||||
|
|
||||||
|
public string CreateToken(AppUser user)
|
||||||
|
{
|
||||||
|
var claims = new List<Claim>
|
||||||
|
{
|
||||||
|
new Claim(JwtRegisteredClaimNames.NameId, user.UserName)
|
||||||
|
};
|
||||||
|
|
||||||
|
var creds = new SigningCredentials(_key, SecurityAlgorithms.HmacSha512Signature);
|
||||||
|
|
||||||
|
var tokenDescriptor = new SecurityTokenDescriptor()
|
||||||
|
{
|
||||||
|
Subject = new ClaimsIdentity(claims),
|
||||||
|
Expires = DateTime.Now.AddDays(7),
|
||||||
|
SigningCredentials = creds
|
||||||
|
};
|
||||||
|
|
||||||
|
var tokenHandler = new JwtSecurityTokenHandler();
|
||||||
|
var token = tokenHandler.CreateToken(tokenDescriptor);
|
||||||
|
|
||||||
|
return tokenHandler.WriteToken(token);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,33 +1,31 @@
|
|||||||
using System;
|
using API.Extensions;
|
||||||
using System.Collections.Generic;
|
using API.Middleware;
|
||||||
using System.Linq;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using Microsoft.AspNetCore.Builder;
|
using Microsoft.AspNetCore.Builder;
|
||||||
using Microsoft.AspNetCore.Hosting;
|
using Microsoft.AspNetCore.Hosting;
|
||||||
using Microsoft.AspNetCore.HttpsPolicy;
|
|
||||||
using Microsoft.AspNetCore.Mvc;
|
|
||||||
using Microsoft.Extensions.Configuration;
|
using Microsoft.Extensions.Configuration;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Microsoft.Extensions.Hosting;
|
using Microsoft.Extensions.Hosting;
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
using Microsoft.OpenApi.Models;
|
using Microsoft.OpenApi.Models;
|
||||||
|
|
||||||
namespace API
|
namespace API
|
||||||
{
|
{
|
||||||
public class Startup
|
public class Startup
|
||||||
{
|
{
|
||||||
public Startup(IConfiguration configuration)
|
private readonly IConfiguration _config;
|
||||||
{
|
|
||||||
Configuration = configuration;
|
|
||||||
}
|
|
||||||
|
|
||||||
public IConfiguration Configuration { get; }
|
public Startup(IConfiguration config)
|
||||||
|
{
|
||||||
|
_config = config;
|
||||||
|
}
|
||||||
|
|
||||||
// This method gets called by the runtime. Use this method to add services to the container.
|
// This method gets called by the runtime. Use this method to add services to the container.
|
||||||
public void ConfigureServices(IServiceCollection services)
|
public void ConfigureServices(IServiceCollection services)
|
||||||
{
|
{
|
||||||
|
|
||||||
|
services.AddApplicationServices(_config);
|
||||||
services.AddControllers();
|
services.AddControllers();
|
||||||
|
services.AddCors();
|
||||||
|
services.AddIdentityServices(_config);
|
||||||
services.AddSwaggerGen(c =>
|
services.AddSwaggerGen(c =>
|
||||||
{
|
{
|
||||||
c.SwaggerDoc("v1", new OpenApiInfo { Title = "API", Version = "v1" });
|
c.SwaggerDoc("v1", new OpenApiInfo { Title = "API", Version = "v1" });
|
||||||
@ -37,9 +35,11 @@ namespace API
|
|||||||
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
|
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
|
||||||
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
|
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
|
||||||
{
|
{
|
||||||
|
app.UseMiddleware<ExceptionMiddleware>();
|
||||||
|
|
||||||
if (env.IsDevelopment())
|
if (env.IsDevelopment())
|
||||||
{
|
{
|
||||||
app.UseDeveloperExceptionPage();
|
//app.UseDeveloperExceptionPage();
|
||||||
app.UseSwagger();
|
app.UseSwagger();
|
||||||
app.UseSwaggerUI(c => c.SwaggerEndpoint("/swagger/v1/swagger.json", "API v1"));
|
app.UseSwaggerUI(c => c.SwaggerEndpoint("/swagger/v1/swagger.json", "API v1"));
|
||||||
}
|
}
|
||||||
@ -47,6 +47,11 @@ namespace API
|
|||||||
app.UseHttpsRedirection();
|
app.UseHttpsRedirection();
|
||||||
|
|
||||||
app.UseRouting();
|
app.UseRouting();
|
||||||
|
|
||||||
|
// Ordering is important. Cors, authentication, authorization
|
||||||
|
app.UseCors(policy => policy.AllowAnyHeader().AllowAnyMethod().WithOrigins("https://localhost:4200"));
|
||||||
|
|
||||||
|
app.UseAuthentication();
|
||||||
|
|
||||||
app.UseAuthorization();
|
app.UseAuthorization();
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user