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('/');
services.AddControllers();
// services.AddDbContext<IdentityDatabase>(options =>
// {
// options.UseNpgsql(_configuration.GetDatabaseConnection("postgres"));
@ -84,6 +86,8 @@ namespace Kyoo.Authentication
// .AddEntityFrameworkStores<IdentityDatabase>();
services.Configure<PermissionOption>(_configuration.GetSection(PermissionOption.Path));
services.Configure<CertificateOption>(_configuration.GetSection(CertificateOption.Path));
services.Configure<AuthenticationOption>(_configuration.GetSection(AuthenticationOption.Path));
CertificateOption certificateOptions = new();
_configuration.GetSection(CertificateOption.Path).Bind(certificateOptions);

View File

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

View File

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

View File

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

View File

@ -2,18 +2,20 @@ using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using IdentityServer4.Extensions;
using IdentityServer4.Models;
using IdentityServer4.Services;
using Kyoo.Authentication.Models;
using Kyoo.Authentication.Models.DTO;
using Kyoo.Controllers;
using Kyoo.Models;
using Kyoo.Models.Exceptions;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Options;
using AuthenticationOptions = Kyoo.Authentication.Models.AuthenticationOptions;
namespace Kyoo.Authentication.Views
{
@ -32,7 +34,7 @@ namespace Kyoo.Authentication.Views
/// <summary>
/// The identity server interaction service to login users.
/// </summary>
private readonly IIdentityServerInteractionService _interaction;
// private readonly IIdentityServerInteractionService _interaction;
/// <summary>
/// A file manager to send profile pictures
/// </summary>
@ -41,7 +43,7 @@ namespace Kyoo.Authentication.Views
/// <summary>
/// Options about authentication. Those options are monitored and reloads are supported.
/// </summary>
private readonly IOptionsMonitor<AuthenticationOptions> _options;
private readonly IOptions<AuthenticationOption> _options;
/// <summary>
@ -52,12 +54,12 @@ namespace Kyoo.Authentication.Views
/// <param name="files">A file manager to send profile pictures</param>
/// <param name="options">Authentication options (this may be hot reloaded)</param>
public AccountApi(IUserRepository users,
IIdentityServerInteractionService interaction,
// IIdentityServerInteractionService interaction,
IFileManager files,
IOptionsMonitor<AuthenticationOptions> options)
IOptions<AuthenticationOption> options)
{
_users = users;
_interaction = interaction;
// _interaction = interaction;
_files = files;
_options = options;
}
@ -69,15 +71,23 @@ namespace Kyoo.Authentication.Views
/// <param name="request">The DTO register request</param>
/// <returns>A OTAC to connect to this new account</returns>
[HttpPost("register")]
public async Task<ActionResult<string>> Register([FromBody] RegisterRequest request)
public async Task<IActionResult> Register([FromBody] RegisterRequest request)
{
User user = request.ToUser();
user.Permissions = _options.CurrentValue.Permissions.NewUser;
user.Permissions = _options.Value.Permissions.NewUser;
user.Password = PasswordUtils.HashPassword(user.Password);
user.ExtraData["otac"] = PasswordUtils.GenerateOTAC();
user.ExtraData["otac-expire"] = DateTime.Now.AddMinutes(1).ToString("s");
await _users.Create(user);
return user.ExtraData["otac"];
try
{
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>
@ -103,10 +113,10 @@ namespace Kyoo.Authentication.Views
[HttpPost("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);
if (context == null || user == null)
if (user == null)
return Unauthorized();
if (!PasswordUtils.CheckPassword(login.Password, user.Password))
return Unauthorized();
@ -122,7 +132,9 @@ namespace Kyoo.Authentication.Views
[HttpPost("otac-login")]
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)
return Unauthorized();
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);
if (user == null)
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);
}
@ -182,7 +194,7 @@ namespace Kyoo.Authentication.Views
user.Username = data.Username;
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 data.Picture.CopyToAsync(file);
}
@ -192,7 +204,7 @@ namespace Kyoo.Authentication.Views
[HttpGet("permissions")]
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 Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.ChangeTracking;
using Microsoft.EntityFrameworkCore.Diagnostics;
namespace Kyoo
{

View File

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

View File

@ -41,11 +41,11 @@ $("#login-btn").on("click", function (e)
success: function ()
{
let returnUrl = new URLSearchParams(window.location.search).get("ReturnUrl");
if (returnUrl == null)
window.location.href = "/unauthorized";
else
window.location.href = returnUrl;
window.location.href = returnUrl;
},
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();
@ -73,7 +73,7 @@ $("#register-btn").on("click", function (e)
error.text("Passwords don't match.");
return;
}
$.ajax(
{
url: "/api/account/register",
@ -81,19 +81,19 @@ $("#register-btn").on("click", function (e)
contentType: 'application/json;charset=UTF-8',
dataType: 'json',
data: JSON.stringify(user),
success: function(res)
success: function(res)
{
useOtac(res.otac);
},
error: function(xhr)
error: function(xhr)
{
let error = $("#register-error");
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)
{
$.ajax(
@ -101,7 +101,7 @@ function useOtac(otac)
url: "/api/account/otac-login",
type: "POST",
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()
{
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");
if (otac != null)
useOtac(otac);
useOtac(otac);

View File

@ -41,6 +41,15 @@ namespace Kyoo.Controllers
.Take(20)
.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 />
public override async Task Delete(User obj)

View File

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

View File

@ -1,5 +1,6 @@
using System;
using System.IO;
using Kyoo.Authentication;
using Kyoo.Controllers;
using Kyoo.Models;
using Kyoo.Postgresql;
@ -46,7 +47,7 @@ namespace Kyoo
_configuration = configuration;
_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>
@ -126,19 +127,20 @@ namespace Kyoo
app.UseResponseCompression();
_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 =>
{
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",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information",
"Microsoft.EntityFrameworkCore.DbUpdateException": "None",
"Microsoft.EntityFrameworkCore.Update": "None",
"Microsoft.EntityFrameworkCore.Database.Command": "None",
"Kyoo": "Trace"
}
},
"dotnet-ef": "false"
},
"authentication": {