diff --git a/Kyoo.Authentication/AuthenticationModule.cs b/Kyoo.Authentication/AuthenticationModule.cs index 02a0f982..559517d2 100644 --- a/Kyoo.Authentication/AuthenticationModule.cs +++ b/Kyoo.Authentication/AuthenticationModule.cs @@ -70,6 +70,8 @@ namespace Kyoo.Authentication { string publicUrl = _configuration.GetValue("public_url").TrimEnd('/'); + services.AddControllers(); + // services.AddDbContext(options => // { // options.UseNpgsql(_configuration.GetDatabaseConnection("postgres")); @@ -84,6 +86,8 @@ namespace Kyoo.Authentication // .AddEntityFrameworkStores(); services.Configure(_configuration.GetSection(PermissionOption.Path)); + services.Configure(_configuration.GetSection(CertificateOption.Path)); + services.Configure(_configuration.GetSection(AuthenticationOption.Path)); CertificateOption certificateOptions = new(); _configuration.GetSection(CertificateOption.Path).Bind(certificateOptions); diff --git a/Kyoo.Authentication/Models/DTO/AccountUpdateRequest.cs b/Kyoo.Authentication/Models/DTO/AccountUpdateRequest.cs index 4ec474d5..ac135799 100644 --- a/Kyoo.Authentication/Models/DTO/AccountUpdateRequest.cs +++ b/Kyoo.Authentication/Models/DTO/AccountUpdateRequest.cs @@ -11,13 +11,13 @@ namespace Kyoo.Authentication.Models.DTO /// /// The new email address of the user /// - [EmailAddress] + [EmailAddress(ErrorMessage = "The email is invalid.")] public string Email { get; set; } /// /// The new username of the user. /// - [MinLength(4)] + [MinLength(4, ErrorMessage = "The username must have at least 4 characters")] public string Username { get; set; } /// diff --git a/Kyoo.Authentication/Models/DTO/RegisterRequest.cs b/Kyoo.Authentication/Models/DTO/RegisterRequest.cs index 4d8efa2c..ad556f6d 100644 --- a/Kyoo.Authentication/Models/DTO/RegisterRequest.cs +++ b/Kyoo.Authentication/Models/DTO/RegisterRequest.cs @@ -1,3 +1,4 @@ +using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using Kyoo.Models; @@ -11,19 +12,19 @@ namespace Kyoo.Authentication.Models.DTO /// /// The user email address /// - [EmailAddress] + [EmailAddress(ErrorMessage = "The email must be a valid email address")] public string Email { get; set; } /// /// The user's username. /// - [MinLength(4)] + [MinLength(4, ErrorMessage = "The username must have at least {1} characters")] public string Username { get; set; } /// /// The user's password. /// - [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() }; } } diff --git a/Kyoo.Authentication/Models/Options/AuthenticationOptions.cs b/Kyoo.Authentication/Models/Options/AuthenticationOption.cs similarity index 94% rename from Kyoo.Authentication/Models/Options/AuthenticationOptions.cs rename to Kyoo.Authentication/Models/Options/AuthenticationOption.cs index f35e22a3..23e917aa 100644 --- a/Kyoo.Authentication/Models/Options/AuthenticationOptions.cs +++ b/Kyoo.Authentication/Models/Options/AuthenticationOption.cs @@ -3,7 +3,7 @@ namespace Kyoo.Authentication.Models /// /// The main authentication options. /// - public class AuthenticationOptions + public class AuthenticationOption { /// /// The path to get this option from the root configuration. diff --git a/Kyoo.Authentication/Views/AccountApi.cs b/Kyoo.Authentication/Views/AccountApi.cs index 903c7e93..bd49da94 100644 --- a/Kyoo.Authentication/Views/AccountApi.cs +++ b/Kyoo.Authentication/Views/AccountApi.cs @@ -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 /// /// The identity server interaction service to login users. /// - private readonly IIdentityServerInteractionService _interaction; + // private readonly IIdentityServerInteractionService _interaction; /// /// A file manager to send profile pictures /// @@ -41,7 +43,7 @@ namespace Kyoo.Authentication.Views /// /// Options about authentication. Those options are monitored and reloads are supported. /// - private readonly IOptionsMonitor _options; + private readonly IOptions _options; /// @@ -52,12 +54,12 @@ namespace Kyoo.Authentication.Views /// A file manager to send profile pictures /// Authentication options (this may be hot reloaded) public AccountApi(IUserRepository users, - IIdentityServerInteractionService interaction, + // IIdentityServerInteractionService interaction, IFileManager files, - IOptionsMonitor options) + IOptions options) { _users = users; - _interaction = interaction; + // _interaction = interaction; _files = files; _options = options; } @@ -69,15 +71,23 @@ namespace Kyoo.Authentication.Views /// The DTO register request /// A OTAC to connect to this new account [HttpPost("register")] - public async Task> Register([FromBody] RegisterRequest request) + public async Task 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"]}); } /// @@ -103,10 +113,10 @@ namespace Kyoo.Authentication.Views [HttpPost("login")] public async Task 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 OtacLogin([FromBody] OtacRequest otac) { - User user = await _users.Get(x => x.ExtraData["OTAC"] == otac.Otac); + // TODO once hstore (Dictionary 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> GetDefaultPermissions() { - return _options.CurrentValue.Permissions.Default; + return _options.Value.Permissions.Default; } } } \ No newline at end of file diff --git a/Kyoo.CommonAPI/DatabaseContext.cs b/Kyoo.CommonAPI/DatabaseContext.cs index 584230b9..6b1bac47 100644 --- a/Kyoo.CommonAPI/DatabaseContext.cs +++ b/Kyoo.CommonAPI/DatabaseContext.cs @@ -8,6 +8,7 @@ using Kyoo.Models; using Kyoo.Models.Exceptions; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.ChangeTracking; +using Microsoft.EntityFrameworkCore.Diagnostics; namespace Kyoo { diff --git a/Kyoo.Postgresql/PostgresModule.cs b/Kyoo.Postgresql/PostgresModule.cs index 7a818296..506f6dbe 100644 --- a/Kyoo.Postgresql/PostgresModule.cs +++ b/Kyoo.Postgresql/PostgresModule.cs @@ -63,7 +63,7 @@ namespace Kyoo.Postgresql services.AddDbContext(x => { x.UseNpgsql(_configuration.GetDatabaseConnection("postgres")); - if (_environment.IsDevelopment()) + if (_configuration.GetValue("logging:dotnet-ef")) x.EnableDetailedErrors().EnableSensitiveDataLogging(); }); // services.AddScoped(_ => new PostgresContext( diff --git a/Kyoo.WebLogin/login.js b/Kyoo.WebLogin/login.js index 36ff3697..973128c8 100644 --- a/Kyoo.WebLogin/login.js +++ b/Kyoo.WebLogin/login.js @@ -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("
")); } }); }); - + 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); \ No newline at end of file + useOtac(otac); diff --git a/Kyoo/Controllers/Repositories/UserRepository.cs b/Kyoo/Controllers/Repositories/UserRepository.cs index 97bce050..5ebd4d2b 100644 --- a/Kyoo/Controllers/Repositories/UserRepository.cs +++ b/Kyoo/Controllers/Repositories/UserRepository.cs @@ -41,6 +41,15 @@ namespace Kyoo.Controllers .Take(20) .ToListAsync(); } + + /// + public override async Task 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; + } /// public override async Task Delete(User obj) diff --git a/Kyoo/Kyoo.csproj b/Kyoo/Kyoo.csproj index 5804420f..7c39d8d3 100644 --- a/Kyoo/Kyoo.csproj +++ b/Kyoo/Kyoo.csproj @@ -45,7 +45,7 @@ - all + diff --git a/Kyoo/Startup.cs b/Kyoo/Startup.cs index 3c7ac57e..0054c066 100644 --- a/Kyoo/Startup.cs +++ b/Kyoo/Startup.cs @@ -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()); - _plugins.LoadPlugins(new IPlugin[] {new CoreModule(), new PostgresModule(configuration, host)}); + _plugins.LoadPlugins(new IPlugin[] {new CoreModule(), new PostgresModule(configuration, host), new AuthenticationModule(configuration, loggerFactory)}); } /// @@ -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"); + }); } } } diff --git a/Kyoo/settings.json b/Kyoo/settings.json index b521f8a8..cc213cc0 100644 --- a/Kyoo/settings.json +++ b/Kyoo/settings.json @@ -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": {