Fixing User database gestion

This commit is contained in:
Zoe Roux 2021-05-08 17:55:22 +02:00
parent 77231f4f41
commit 429af9b252
12 changed files with 80 additions and 46 deletions

View File

@ -70,6 +70,8 @@ namespace Kyoo.Authentication
{ {
string publicUrl = _configuration.GetValue<string>("public_url").TrimEnd('/'); string publicUrl = _configuration.GetValue<string>("public_url").TrimEnd('/');
services.AddControllers();
// services.AddDbContext<IdentityDatabase>(options => // services.AddDbContext<IdentityDatabase>(options =>
// { // {
// options.UseNpgsql(_configuration.GetDatabaseConnection("postgres")); // options.UseNpgsql(_configuration.GetDatabaseConnection("postgres"));
@ -84,6 +86,8 @@ namespace Kyoo.Authentication
// .AddEntityFrameworkStores<IdentityDatabase>(); // .AddEntityFrameworkStores<IdentityDatabase>();
services.Configure<PermissionOption>(_configuration.GetSection(PermissionOption.Path)); services.Configure<PermissionOption>(_configuration.GetSection(PermissionOption.Path));
services.Configure<CertificateOption>(_configuration.GetSection(CertificateOption.Path));
services.Configure<AuthenticationOption>(_configuration.GetSection(AuthenticationOption.Path));
CertificateOption certificateOptions = new(); CertificateOption certificateOptions = new();
_configuration.GetSection(CertificateOption.Path).Bind(certificateOptions); _configuration.GetSection(CertificateOption.Path).Bind(certificateOptions);

View File

@ -11,13 +11,13 @@ namespace Kyoo.Authentication.Models.DTO
/// <summary> /// <summary>
/// The new email address of the user /// The new email address of the user
/// </summary> /// </summary>
[EmailAddress] [EmailAddress(ErrorMessage = "The email is invalid.")]
public string Email { get; set; } public string Email { get; set; }
/// <summary> /// <summary>
/// The new username of the user. /// The new username of the user.
/// </summary> /// </summary>
[MinLength(4)] [MinLength(4, ErrorMessage = "The username must have at least 4 characters")]
public string Username { get; set; } public string Username { get; set; }
/// <summary> /// <summary>

View File

@ -1,3 +1,4 @@
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using Kyoo.Models; using Kyoo.Models;
@ -11,19 +12,19 @@ namespace Kyoo.Authentication.Models.DTO
/// <summary> /// <summary>
/// The user email address /// The user email address
/// </summary> /// </summary>
[EmailAddress] [EmailAddress(ErrorMessage = "The email must be a valid email address")]
public string Email { get; set; } public string Email { get; set; }
/// <summary> /// <summary>
/// The user's username. /// The user's username.
/// </summary> /// </summary>
[MinLength(4)] [MinLength(4, ErrorMessage = "The username must have at least {1} characters")]
public string Username { get; set; } public string Username { get; set; }
/// <summary> /// <summary>
/// The user's password. /// The user's password.
/// </summary> /// </summary>
[MinLength(8)] [MinLength(8, ErrorMessage = "The password must have at least {1} characters")]
public string Password { get; set; } public string Password { get; set; }
@ -38,7 +39,8 @@ namespace Kyoo.Authentication.Models.DTO
Slug = Utility.ToSlug(Username), Slug = Utility.ToSlug(Username),
Username = Username, Username = Username,
Password = Password, Password = Password,
Email = Email Email = Email,
ExtraData = new Dictionary<string, string>()
}; };
} }
} }

View File

@ -3,7 +3,7 @@ namespace Kyoo.Authentication.Models
/// <summary> /// <summary>
/// The main authentication options. /// The main authentication options.
/// </summary> /// </summary>
public class AuthenticationOptions public class AuthenticationOption
{ {
/// <summary> /// <summary>
/// The path to get this option from the root configuration. /// The path to get this option from the root configuration.

View File

@ -2,18 +2,20 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Globalization; using System.Globalization;
using System.IO; using System.IO;
using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using IdentityServer4.Extensions; using IdentityServer4.Extensions;
using IdentityServer4.Models; using IdentityServer4.Models;
using IdentityServer4.Services; using IdentityServer4.Services;
using Kyoo.Authentication.Models;
using Kyoo.Authentication.Models.DTO; using Kyoo.Authentication.Models.DTO;
using Kyoo.Controllers; using Kyoo.Controllers;
using Kyoo.Models; using Kyoo.Models;
using Kyoo.Models.Exceptions;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using AuthenticationOptions = Kyoo.Authentication.Models.AuthenticationOptions;
namespace Kyoo.Authentication.Views namespace Kyoo.Authentication.Views
{ {
@ -32,7 +34,7 @@ namespace Kyoo.Authentication.Views
/// <summary> /// <summary>
/// The identity server interaction service to login users. /// The identity server interaction service to login users.
/// </summary> /// </summary>
private readonly IIdentityServerInteractionService _interaction; // private readonly IIdentityServerInteractionService _interaction;
/// <summary> /// <summary>
/// A file manager to send profile pictures /// A file manager to send profile pictures
/// </summary> /// </summary>
@ -41,7 +43,7 @@ namespace Kyoo.Authentication.Views
/// <summary> /// <summary>
/// Options about authentication. Those options are monitored and reloads are supported. /// Options about authentication. Those options are monitored and reloads are supported.
/// </summary> /// </summary>
private readonly IOptionsMonitor<AuthenticationOptions> _options; private readonly IOptions<AuthenticationOption> _options;
/// <summary> /// <summary>
@ -52,12 +54,12 @@ namespace Kyoo.Authentication.Views
/// <param name="files">A file manager to send profile pictures</param> /// <param name="files">A file manager to send profile pictures</param>
/// <param name="options">Authentication options (this may be hot reloaded)</param> /// <param name="options">Authentication options (this may be hot reloaded)</param>
public AccountApi(IUserRepository users, public AccountApi(IUserRepository users,
IIdentityServerInteractionService interaction, // IIdentityServerInteractionService interaction,
IFileManager files, IFileManager files,
IOptionsMonitor<AuthenticationOptions> options) IOptions<AuthenticationOption> options)
{ {
_users = users; _users = users;
_interaction = interaction; // _interaction = interaction;
_files = files; _files = files;
_options = options; _options = options;
} }
@ -69,15 +71,23 @@ namespace Kyoo.Authentication.Views
/// <param name="request">The DTO register request</param> /// <param name="request">The DTO register request</param>
/// <returns>A OTAC to connect to this new account</returns> /// <returns>A OTAC to connect to this new account</returns>
[HttpPost("register")] [HttpPost("register")]
public async Task<ActionResult<string>> Register([FromBody] RegisterRequest request) public async Task<IActionResult> Register([FromBody] RegisterRequest request)
{ {
User user = request.ToUser(); User user = request.ToUser();
user.Permissions = _options.CurrentValue.Permissions.NewUser; user.Permissions = _options.Value.Permissions.NewUser;
user.Password = PasswordUtils.HashPassword(user.Password); user.Password = PasswordUtils.HashPassword(user.Password);
user.ExtraData["otac"] = PasswordUtils.GenerateOTAC(); user.ExtraData["otac"] = PasswordUtils.GenerateOTAC();
user.ExtraData["otac-expire"] = DateTime.Now.AddMinutes(1).ToString("s"); user.ExtraData["otac-expire"] = DateTime.Now.AddMinutes(1).ToString("s");
await _users.Create(user); try
return user.ExtraData["otac"]; {
await _users.Create(user);
}
catch (DuplicatedItemException)
{
return Conflict(new {Errors = new {Duplicate = new[] {"A user with this name already exists"}}});
}
return Ok(new {Otac = user.ExtraData["otac"]});
} }
/// <summary> /// <summary>
@ -103,10 +113,10 @@ namespace Kyoo.Authentication.Views
[HttpPost("login")] [HttpPost("login")]
public async Task<IActionResult> Login([FromBody] LoginRequest login) public async Task<IActionResult> Login([FromBody] LoginRequest login)
{ {
AuthorizationRequest context = await _interaction.GetAuthorizationContextAsync(login.ReturnURL); // AuthorizationRequest context = await _interaction.GetAuthorizationContextAsync(login.ReturnURL);
User user = await _users.Get(x => x.Username == login.Username); User user = await _users.Get(x => x.Username == login.Username);
if (context == null || user == null) if (user == null)
return Unauthorized(); return Unauthorized();
if (!PasswordUtils.CheckPassword(login.Password, user.Password)) if (!PasswordUtils.CheckPassword(login.Password, user.Password))
return Unauthorized(); return Unauthorized();
@ -122,7 +132,9 @@ namespace Kyoo.Authentication.Views
[HttpPost("otac-login")] [HttpPost("otac-login")]
public async Task<IActionResult> OtacLogin([FromBody] OtacRequest otac) public async Task<IActionResult> OtacLogin([FromBody] OtacRequest otac)
{ {
User user = await _users.Get(x => x.ExtraData["OTAC"] == otac.Otac); // TODO once hstore (Dictionary<string, string> accessor) are supported, use them.
// We retrieve all users, this is inefficient.
User user = (await _users.GetAll()).FirstOrDefault(x => x.ExtraData.GetValueOrDefault("otac") == otac.Otac);
if (user == null) if (user == null)
return Unauthorized(); return Unauthorized();
if (DateTime.ParseExact(user.ExtraData["otac-expire"], "s", CultureInfo.InvariantCulture) <= DateTime.UtcNow) if (DateTime.ParseExact(user.ExtraData["otac-expire"], "s", CultureInfo.InvariantCulture) <= DateTime.UtcNow)
@ -166,7 +178,7 @@ namespace Kyoo.Authentication.Views
User user = await _users.GetOrDefault(slug); User user = await _users.GetOrDefault(slug);
if (user == null) if (user == null)
return NotFound(); return NotFound();
string path = Path.Combine(_options.CurrentValue.ProfilePicturePath, user.ID.ToString()); string path = Path.Combine(_options.Value.ProfilePicturePath, user.ID.ToString());
return _files.FileResult(path); return _files.FileResult(path);
} }
@ -182,7 +194,7 @@ namespace Kyoo.Authentication.Views
user.Username = data.Username; user.Username = data.Username;
if (data.Picture?.Length > 0) if (data.Picture?.Length > 0)
{ {
string path = Path.Combine(_options.CurrentValue.ProfilePicturePath, user.ID.ToString()); string path = Path.Combine(_options.Value.ProfilePicturePath, user.ID.ToString());
await using Stream file = _files.NewFile(path); await using Stream file = _files.NewFile(path);
await data.Picture.CopyToAsync(file); await data.Picture.CopyToAsync(file);
} }
@ -192,7 +204,7 @@ namespace Kyoo.Authentication.Views
[HttpGet("permissions")] [HttpGet("permissions")]
public ActionResult<IEnumerable<string>> GetDefaultPermissions() public ActionResult<IEnumerable<string>> GetDefaultPermissions()
{ {
return _options.CurrentValue.Permissions.Default; return _options.Value.Permissions.Default;
} }
} }
} }

View File

@ -8,6 +8,7 @@ using Kyoo.Models;
using Kyoo.Models.Exceptions; using Kyoo.Models.Exceptions;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.ChangeTracking; using Microsoft.EntityFrameworkCore.ChangeTracking;
using Microsoft.EntityFrameworkCore.Diagnostics;
namespace Kyoo namespace Kyoo
{ {

View File

@ -63,7 +63,7 @@ namespace Kyoo.Postgresql
services.AddDbContext<DatabaseContext, PostgresContext>(x => services.AddDbContext<DatabaseContext, PostgresContext>(x =>
{ {
x.UseNpgsql(_configuration.GetDatabaseConnection("postgres")); x.UseNpgsql(_configuration.GetDatabaseConnection("postgres"));
if (_environment.IsDevelopment()) if (_configuration.GetValue<bool>("logging:dotnet-ef"))
x.EnableDetailedErrors().EnableSensitiveDataLogging(); x.EnableDetailedErrors().EnableSensitiveDataLogging();
}); });
// services.AddScoped<DatabaseContext>(_ => new PostgresContext( // services.AddScoped<DatabaseContext>(_ => new PostgresContext(

View File

@ -41,11 +41,11 @@ $("#login-btn").on("click", function (e)
success: function () success: function ()
{ {
let returnUrl = new URLSearchParams(window.location.search).get("ReturnUrl"); let returnUrl = new URLSearchParams(window.location.search).get("ReturnUrl");
if (returnUrl == null) if (returnUrl == null)
window.location.href = "/unauthorized"; window.location.href = "/unauthorized";
else else
window.location.href = returnUrl; window.location.href = returnUrl;
}, },
error: function(xhr) error: function(xhr)
{ {
@ -56,7 +56,7 @@ $("#login-btn").on("click", function (e)
}); });
}); });
$("#register-btn").on("click", function (e) $("#register-btn").on("click", function (e)
{ {
e.preventDefault(); e.preventDefault();
@ -73,7 +73,7 @@ $("#register-btn").on("click", function (e)
error.text("Passwords don't match."); error.text("Passwords don't match.");
return; return;
} }
$.ajax( $.ajax(
{ {
url: "/api/account/register", url: "/api/account/register",
@ -81,19 +81,19 @@ $("#register-btn").on("click", function (e)
contentType: 'application/json;charset=UTF-8', contentType: 'application/json;charset=UTF-8',
dataType: 'json', dataType: 'json',
data: JSON.stringify(user), data: JSON.stringify(user),
success: function(res) success: function(res)
{ {
useOtac(res.otac); useOtac(res.otac);
}, },
error: function(xhr) error: function(xhr)
{ {
let error = $("#register-error"); let error = $("#register-error");
error.show(); error.show();
error.text(JSON.parse(xhr.responseText)[0].description); error.html(Object.values(JSON.parse(xhr.responseText).errors).map(x => x[0]).join("<br/>"));
} }
}); });
}); });
function useOtac(otac) function useOtac(otac)
{ {
$.ajax( $.ajax(
@ -101,7 +101,7 @@ function useOtac(otac)
url: "/api/account/otac-login", url: "/api/account/otac-login",
type: "POST", type: "POST",
contentType: 'application/json;charset=UTF-8', contentType: 'application/json;charset=UTF-8',
data: JSON.stringify({otac: otac, tayLoggedIn: $("#stay-logged-in")[0].checked}), data: JSON.stringify({otac: otac, stayLoggedIn: $("#stay-logged-in")[0].checked}),
success: function() success: function()
{ {
let returnUrl = new URLSearchParams(window.location.search).get("ReturnUrl"); let returnUrl = new URLSearchParams(window.location.search).get("ReturnUrl");
@ -124,4 +124,4 @@ function useOtac(otac)
let otac = new URLSearchParams(window.location.search).get("otac"); let otac = new URLSearchParams(window.location.search).get("otac");
if (otac != null) if (otac != null)
useOtac(otac); useOtac(otac);

View File

@ -41,6 +41,15 @@ namespace Kyoo.Controllers
.Take(20) .Take(20)
.ToListAsync(); .ToListAsync();
} }
/// <inheritdoc />
public override async Task<User> Create(User obj)
{
await base.Create(obj);
_database.Entry(obj).State = EntityState.Added;
await _database.SaveChangesAsync($"Trying to insert a duplicated user (slug {obj.Slug} already exists).");
return obj;
}
/// <inheritdoc /> /// <inheritdoc />
public override async Task Delete(User obj) public override async Task Delete(User obj)

View File

@ -45,7 +45,7 @@
<ItemGroup> <ItemGroup>
<ProjectReference Include="../Kyoo.Authentication/Kyoo.Authentication.csproj"> <ProjectReference Include="../Kyoo.Authentication/Kyoo.Authentication.csproj">
<ExcludeAssets>all</ExcludeAssets> <!-- <ExcludeAssets>all</ExcludeAssets>-->
</ProjectReference> </ProjectReference>
</ItemGroup> </ItemGroup>

View File

@ -1,5 +1,6 @@
using System; using System;
using System.IO; using System.IO;
using Kyoo.Authentication;
using Kyoo.Controllers; using Kyoo.Controllers;
using Kyoo.Models; using Kyoo.Models;
using Kyoo.Postgresql; using Kyoo.Postgresql;
@ -46,7 +47,7 @@ namespace Kyoo
_configuration = configuration; _configuration = configuration;
_plugins = new PluginManager(hostProvider, _configuration, loggerFactory.CreateLogger<PluginManager>()); _plugins = new PluginManager(hostProvider, _configuration, loggerFactory.CreateLogger<PluginManager>());
_plugins.LoadPlugins(new IPlugin[] {new CoreModule(), new PostgresModule(configuration, host)}); _plugins.LoadPlugins(new IPlugin[] {new CoreModule(), new PostgresModule(configuration, host), new AuthenticationModule(configuration, loggerFactory)});
} }
/// <summary> /// <summary>
@ -126,19 +127,20 @@ namespace Kyoo
app.UseResponseCompression(); app.UseResponseCompression();
_plugins.ConfigureAspnet(app); _plugins.ConfigureAspnet(app);
//
// app.UseSpa(spa =>
// {
// spa.Options.SourcePath = Path.Join(AppDomain.CurrentDomain.BaseDirectory, "Kyoo.WebApp");
//
// if (env.IsDevelopment())
// spa.UseAngularCliServer("start");
// });
app.UseEndpoints(endpoints => app.UseEndpoints(endpoints =>
{ {
endpoints.MapControllerRoute("Kyoo", "api/{controller=Home}/{action=Index}/{id?}"); endpoints.MapControllerRoute("Kyoo", "api/{controller=Home}/{action=Index}/{id?}");
}); });
app.UseSpa(spa =>
{
spa.Options.SourcePath = Path.Join(AppDomain.CurrentDomain.BaseDirectory, "Kyoo.WebApp");
if (env.IsDevelopment())
spa.UseAngularCliServer("start");
});
} }
} }
} }

View File

@ -20,8 +20,12 @@
"default": "Trace", "default": "Trace",
"Microsoft": "Warning", "Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information", "Microsoft.Hosting.Lifetime": "Information",
"Microsoft.EntityFrameworkCore.DbUpdateException": "None",
"Microsoft.EntityFrameworkCore.Update": "None",
"Microsoft.EntityFrameworkCore.Database.Command": "None",
"Kyoo": "Trace" "Kyoo": "Trace"
} },
"dotnet-ef": "false"
}, },
"authentication": { "authentication": {