mirror of
https://github.com/Kareadita/Kavita.git
synced 2025-05-24 00:52:23 -04:00
Setup basic code for login.
This commit is contained in:
parent
a2e6d03d5b
commit
2b521924d0
82
API/Controllers/AccountController.cs
Normal file
82
API/Controllers/AccountController.cs
Normal file
@ -0,0 +1,82 @@
|
||||
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 ILogger<AccountController> _logger;
|
||||
|
||||
public AccountController(DataContext context, ITokenService tokenService, ILogger<AccountController> logger)
|
||||
{
|
||||
_context = context;
|
||||
_tokenService = tokenService;
|
||||
_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
|
||||
};
|
||||
|
||||
_context.Users.Add(user);
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
return new UserDto()
|
||||
{
|
||||
Username = user.UserName,
|
||||
Token = _tokenService.CreateToken(user)
|
||||
};
|
||||
}
|
||||
|
||||
[HttpPost("login")]
|
||||
public async Task<ActionResult<UserDto>> Login(LoginDto loginDto)
|
||||
{
|
||||
var user = await _context.Users.SingleOrDefaultAsync(x => x.UserName == loginDto.Username.ToLower());
|
||||
|
||||
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");
|
||||
}
|
||||
|
||||
return new UserDto()
|
||||
{
|
||||
Username = user.UserName,
|
||||
Token = _tokenService.CreateToken(user)
|
||||
};
|
||||
}
|
||||
|
||||
private async Task<bool> UserExists(string username)
|
||||
{
|
||||
return await _context.Users.AnyAsync(user => user.UserName == username.ToLower());
|
||||
}
|
||||
}
|
||||
}
|
13
API/Controllers/BaseApiController.cs
Normal file
13
API/Controllers/BaseApiController.cs
Normal file
@ -0,0 +1,13 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace API.Controllers
|
||||
{
|
||||
[ApiController]
|
||||
[Route("api/[controller]")]
|
||||
public class BaseApiController : ControllerBase
|
||||
{
|
||||
public BaseApiController()
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
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; }
|
||||
}
|
||||
}
|
13
API/DTOs/RegisterDto.cs
Normal file
13
API/DTOs/RegisterDto.cs
Normal file
@ -0,0 +1,13 @@
|
||||
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; }
|
||||
}
|
||||
}
|
8
API/DTOs/UserDto.cs
Normal file
8
API/DTOs/UserDto.cs
Normal file
@ -0,0 +1,8 @@
|
||||
namespace API.DTOs
|
||||
{
|
||||
public class UserDto
|
||||
{
|
||||
public string Username { get; set; }
|
||||
public string Token { 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; }
|
||||
}
|
||||
}
|
28
API/Entities/AppUser.cs
Normal file
28
API/Entities/AppUser.cs
Normal file
@ -0,0 +1,28 @@
|
||||
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; }
|
||||
|
||||
[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;
|
||||
}
|
||||
}
|
||||
}
|
23
API/Extensions/ApplicationServiceExtensions.cs
Normal file
23
API/Extensions/ApplicationServiceExtensions.cs
Normal file
@ -0,0 +1,23 @@
|
||||
using API.Data;
|
||||
using API.Interfaces;
|
||||
using API.Services;
|
||||
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.AddScoped<ITokenService, TokenService>();
|
||||
services.AddDbContext<DataContext>(options =>
|
||||
{
|
||||
options.UseSqlite(config.GetConnectionString("DefaultConnection"));
|
||||
});
|
||||
|
||||
return services;
|
||||
}
|
||||
}
|
||||
}
|
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;
|
||||
}
|
||||
}
|
||||
}
|
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);
|
||||
}
|
||||
}
|
55
API/Middleware/ExceptionMiddleware.cs
Normal file
55
API/Middleware/ExceptionMiddleware.cs
Normal file
@ -0,0 +1,55 @@
|
||||
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?.ToString())
|
||||
: 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);
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
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);
|
||||
}
|
||||
}
|
||||
}
|
@ -2,6 +2,8 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using API.Extensions;
|
||||
using API.Middleware;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.AspNetCore.HttpsPolicy;
|
||||
@ -16,18 +18,21 @@ namespace API
|
||||
{
|
||||
public class Startup
|
||||
{
|
||||
public Startup(IConfiguration configuration)
|
||||
{
|
||||
Configuration = configuration;
|
||||
}
|
||||
private readonly IConfiguration _config;
|
||||
|
||||
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.
|
||||
public void ConfigureServices(IServiceCollection services)
|
||||
{
|
||||
|
||||
services.AddApplicationServices(_config);
|
||||
services.AddControllers();
|
||||
services.AddCors();
|
||||
services.AddIdentityServices(_config);
|
||||
services.AddSwaggerGen(c =>
|
||||
{
|
||||
c.SwaggerDoc("v1", new OpenApiInfo { Title = "API", Version = "v1" });
|
||||
@ -37,9 +42,11 @@ namespace API
|
||||
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
|
||||
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
|
||||
{
|
||||
app.UseMiddleware<ExceptionMiddleware>();
|
||||
|
||||
if (env.IsDevelopment())
|
||||
{
|
||||
app.UseDeveloperExceptionPage();
|
||||
//app.UseDeveloperExceptionPage();
|
||||
app.UseSwagger();
|
||||
app.UseSwaggerUI(c => c.SwaggerEndpoint("/swagger/v1/swagger.json", "API v1"));
|
||||
}
|
||||
@ -47,6 +54,11 @@ namespace API
|
||||
app.UseHttpsRedirection();
|
||||
|
||||
app.UseRouting();
|
||||
|
||||
// Ordering is important. Cors, authentication, authorization
|
||||
app.UseCors(policy => policy.AllowAnyHeader().AllowAnyMethod().WithOrigins("https://localhost:4200"));
|
||||
|
||||
app.UseAuthentication();
|
||||
|
||||
app.UseAuthorization();
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user