From a5f1018fda5572912c126b1e8dd656209fca0e46 Mon Sep 17 00:00:00 2001 From: ivarlovlie Date: Tue, 11 Aug 2020 21:16:02 +0200 Subject: persisted grants --- src/server/Controllers/BaseController.cs | 10 +- src/server/Dough.csproj | 1 + src/server/IdentityServer/Config.cs | 7 +- src/server/IdentityServer/ProfileService.cs | 25 +++- ...11164236_INITIAL_IDENTITY_MIGRATION.Designer.cs | 127 +++++++++++++++++++++ .../20200811164236_INITIAL_IDENTITY_MIGRATION.cs | 85 ++++++++++++++ .../PersistedGrantDbContextModelSnapshot.cs | 125 ++++++++++++++++++++ src/server/Models/Constants.cs | 8 -- src/server/Program.cs | 5 +- src/server/Startup.cs | 75 ++++++++---- src/server/appsettings.json | 10 -- src/server/wwwroot/login.js | 6 +- 12 files changed, 421 insertions(+), 63 deletions(-) create mode 100644 src/server/Migrations/IdentityServer/PersistedGrant/20200811164236_INITIAL_IDENTITY_MIGRATION.Designer.cs create mode 100644 src/server/Migrations/IdentityServer/PersistedGrant/20200811164236_INITIAL_IDENTITY_MIGRATION.cs create mode 100644 src/server/Migrations/IdentityServer/PersistedGrant/PersistedGrantDbContextModelSnapshot.cs delete mode 100644 src/server/appsettings.json (limited to 'src/server') diff --git a/src/server/Controllers/BaseController.cs b/src/server/Controllers/BaseController.cs index 44b11b2..3737174 100644 --- a/src/server/Controllers/BaseController.cs +++ b/src/server/Controllers/BaseController.cs @@ -1,26 +1,26 @@ using System; -using System.Security.Claims; using Microsoft.AspNetCore.Mvc; using Dough.Utilities; +using IdentityModel; +using Microsoft.AspNetCore.Authorization; namespace Dough.Controllers { [ApiController] + [Authorize] [Route("[controller]")] public class BaseController : ControllerBase { public LoggedInUserModel LoggedInUser => new LoggedInUserModel { - Id = User.GetClaimValueOrDefault(ClaimTypes.NameIdentifier)?.ToGuidOrDefault() ?? default, - Username = User.GetClaimValueOrDefault(ClaimTypes.Name), - SessionStart = User.GetClaimValueOrDefault(ClaimTypes.AuthenticationInstant).ToDateTimeOrDefault() + Id = User.GetClaimValueOrDefault(JwtClaimTypes.Subject)?.ToGuidOrDefault() ?? default, + Username = User.GetClaimValueOrDefault(JwtClaimTypes.PreferredUserName), }; public class LoggedInUserModel { public Guid Id { get; set; } public string Username { get; set; } - public DateTime SessionStart { get; set; } } } } diff --git a/src/server/Dough.csproj b/src/server/Dough.csproj index 9a5af5b..c3c3bff 100644 --- a/src/server/Dough.csproj +++ b/src/server/Dough.csproj @@ -8,6 +8,7 @@ + diff --git a/src/server/IdentityServer/Config.cs b/src/server/IdentityServer/Config.cs index c005377..ac38aa4 100644 --- a/src/server/IdentityServer/Config.cs +++ b/src/server/IdentityServer/Config.cs @@ -18,20 +18,15 @@ namespace Dough.IdentityServer ClientId = BrowserClientId, AllowedGrantTypes = GrantTypes.Code, RequireClientSecret = false, - RedirectUris = Constants.BrowserAppLoginRedirectUrls, PostLogoutRedirectUris = Constants.BrowserAppLogoutRedirectUrls, AllowedCorsOrigins = Constants.BrowserAppUrls, AccessTokenType = AccessTokenType.Reference, RequireConsent = false, RefreshTokenExpiration = TokenExpiration.Sliding, + RefreshTokenUsage = TokenUsage.ReUse, AlwaysSendClientClaims = true, AllowOfflineAccess = true, - Claims = new List - { - new ClientClaim() - }, - AllowedScopes = { IdentityServerConstants.StandardScopes.OpenId, diff --git a/src/server/IdentityServer/ProfileService.cs b/src/server/IdentityServer/ProfileService.cs index 197086c..0c14dd5 100644 --- a/src/server/IdentityServer/ProfileService.cs +++ b/src/server/IdentityServer/ProfileService.cs @@ -1,8 +1,11 @@ -using System.Reflection; +using System; +using System.Collections.Generic; +using System.Security.Claims; using System.Threading.Tasks; +using Dough.Models; using Dough.Models.Database; using Dough.Utilities; -using IdentityServer4; +using IdentityModel; using IdentityServer4.Models; using IdentityServer4.Services; @@ -17,14 +20,24 @@ namespace Dough.IdentityServer _context = context; } - public Task GetProfileDataAsync(ProfileDataRequestContext context) + public async Task GetProfileDataAsync(ProfileDataRequestContext context) { - throw new System.NotImplementedException(); + var userId = context.Subject.GetClaimValueOrDefault(JwtClaimTypes.Subject)?.ToGuidOrDefault(); + if (userId == default) return; + var user = _context.Users.SingleOrDefault((Guid) userId); + var claims = new List + { + new Claim(JwtClaimTypes.PreferredUserName, user.Username) + }; + context.AddRequestedClaims(claims); } - public Task IsActiveAsync(IsActiveContext context) + public async Task IsActiveAsync(IsActiveContext context) { - return default; + var userId = context.Subject.GetClaimValueOrDefault(JwtClaimTypes.Subject)?.ToGuidOrDefault(); + if (userId == default) return; + var user = _context.Users.SingleOrDefault((Guid) userId); + context.IsActive = !user.Hidden; } } } \ No newline at end of file diff --git a/src/server/Migrations/IdentityServer/PersistedGrant/20200811164236_INITIAL_IDENTITY_MIGRATION.Designer.cs b/src/server/Migrations/IdentityServer/PersistedGrant/20200811164236_INITIAL_IDENTITY_MIGRATION.Designer.cs new file mode 100644 index 0000000..252ba5b --- /dev/null +++ b/src/server/Migrations/IdentityServer/PersistedGrant/20200811164236_INITIAL_IDENTITY_MIGRATION.Designer.cs @@ -0,0 +1,127 @@ +// +using System; +using IdentityServer4.EntityFramework.DbContexts; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +namespace Dough.Migrations.IdentityServer.PersistedGrant +{ + [DbContext(typeof(PersistedGrantDbContext))] + [Migration("20200811164236_INITIAL_IDENTITY_MIGRATION")] + partial class INITIAL_IDENTITY_MIGRATION + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "3.1.6") + .HasAnnotation("Relational:MaxIdentifierLength", 64); + + modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.DeviceFlowCodes", b => + { + b.Property("UserCode") + .HasColumnType("varchar(200) CHARACTER SET utf8mb4") + .HasMaxLength(200); + + b.Property("ClientId") + .IsRequired() + .HasColumnType("varchar(200) CHARACTER SET utf8mb4") + .HasMaxLength(200); + + b.Property("CreationTime") + .HasColumnType("datetime(6)"); + + b.Property("Data") + .IsRequired() + .HasColumnType("longtext CHARACTER SET utf8mb4") + .HasMaxLength(50000); + + b.Property("Description") + .HasColumnType("varchar(200) CHARACTER SET utf8mb4") + .HasMaxLength(200); + + b.Property("DeviceCode") + .IsRequired() + .HasColumnType("varchar(200) CHARACTER SET utf8mb4") + .HasMaxLength(200); + + b.Property("Expiration") + .IsRequired() + .HasColumnType("datetime(6)"); + + b.Property("SessionId") + .HasColumnType("varchar(100) CHARACTER SET utf8mb4") + .HasMaxLength(100); + + b.Property("SubjectId") + .HasColumnType("varchar(200) CHARACTER SET utf8mb4") + .HasMaxLength(200); + + b.HasKey("UserCode"); + + b.HasIndex("DeviceCode") + .IsUnique(); + + b.HasIndex("Expiration"); + + b.ToTable("DeviceCodes"); + }); + + modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.PersistedGrant", b => + { + b.Property("Key") + .HasColumnType("varchar(200) CHARACTER SET utf8mb4") + .HasMaxLength(200); + + b.Property("ClientId") + .IsRequired() + .HasColumnType("varchar(200) CHARACTER SET utf8mb4") + .HasMaxLength(200); + + b.Property("ConsumedTime") + .HasColumnType("datetime(6)"); + + b.Property("CreationTime") + .HasColumnType("datetime(6)"); + + b.Property("Data") + .IsRequired() + .HasColumnType("longtext CHARACTER SET utf8mb4") + .HasMaxLength(50000); + + b.Property("Description") + .HasColumnType("varchar(200) CHARACTER SET utf8mb4") + .HasMaxLength(200); + + b.Property("Expiration") + .HasColumnType("datetime(6)"); + + b.Property("SessionId") + .HasColumnType("varchar(100) CHARACTER SET utf8mb4") + .HasMaxLength(100); + + b.Property("SubjectId") + .HasColumnType("varchar(200) CHARACTER SET utf8mb4") + .HasMaxLength(200); + + b.Property("Type") + .IsRequired() + .HasColumnType("varchar(50) CHARACTER SET utf8mb4") + .HasMaxLength(50); + + b.HasKey("Key"); + + b.HasIndex("Expiration"); + + b.HasIndex("SubjectId", "ClientId", "Type"); + + b.HasIndex("SubjectId", "SessionId", "Type"); + + b.ToTable("PersistedGrants"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/server/Migrations/IdentityServer/PersistedGrant/20200811164236_INITIAL_IDENTITY_MIGRATION.cs b/src/server/Migrations/IdentityServer/PersistedGrant/20200811164236_INITIAL_IDENTITY_MIGRATION.cs new file mode 100644 index 0000000..5ae6fb2 --- /dev/null +++ b/src/server/Migrations/IdentityServer/PersistedGrant/20200811164236_INITIAL_IDENTITY_MIGRATION.cs @@ -0,0 +1,85 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +namespace Dough.Migrations.IdentityServer.PersistedGrant +{ + public partial class INITIAL_IDENTITY_MIGRATION : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "DeviceCodes", + columns: table => new + { + UserCode = table.Column(maxLength: 200, nullable: false), + DeviceCode = table.Column(maxLength: 200, nullable: false), + SubjectId = table.Column(maxLength: 200, nullable: true), + SessionId = table.Column(maxLength: 100, nullable: true), + ClientId = table.Column(maxLength: 200, nullable: false), + Description = table.Column(maxLength: 200, nullable: true), + CreationTime = table.Column(nullable: false), + Expiration = table.Column(nullable: false), + Data = table.Column(maxLength: 50000, nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_DeviceCodes", x => x.UserCode); + }); + + migrationBuilder.CreateTable( + name: "PersistedGrants", + columns: table => new + { + Key = table.Column(maxLength: 200, nullable: false), + Type = table.Column(maxLength: 50, nullable: false), + SubjectId = table.Column(maxLength: 200, nullable: true), + SessionId = table.Column(maxLength: 100, nullable: true), + ClientId = table.Column(maxLength: 200, nullable: false), + Description = table.Column(maxLength: 200, nullable: true), + CreationTime = table.Column(nullable: false), + Expiration = table.Column(nullable: true), + ConsumedTime = table.Column(nullable: true), + Data = table.Column(maxLength: 50000, nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_PersistedGrants", x => x.Key); + }); + + migrationBuilder.CreateIndex( + name: "IX_DeviceCodes_DeviceCode", + table: "DeviceCodes", + column: "DeviceCode", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_DeviceCodes_Expiration", + table: "DeviceCodes", + column: "Expiration"); + + migrationBuilder.CreateIndex( + name: "IX_PersistedGrants_Expiration", + table: "PersistedGrants", + column: "Expiration"); + + migrationBuilder.CreateIndex( + name: "IX_PersistedGrants_SubjectId_ClientId_Type", + table: "PersistedGrants", + columns: new[] { "SubjectId", "ClientId", "Type" }); + + migrationBuilder.CreateIndex( + name: "IX_PersistedGrants_SubjectId_SessionId_Type", + table: "PersistedGrants", + columns: new[] { "SubjectId", "SessionId", "Type" }); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "DeviceCodes"); + + migrationBuilder.DropTable( + name: "PersistedGrants"); + } + } +} diff --git a/src/server/Migrations/IdentityServer/PersistedGrant/PersistedGrantDbContextModelSnapshot.cs b/src/server/Migrations/IdentityServer/PersistedGrant/PersistedGrantDbContextModelSnapshot.cs new file mode 100644 index 0000000..da76342 --- /dev/null +++ b/src/server/Migrations/IdentityServer/PersistedGrant/PersistedGrantDbContextModelSnapshot.cs @@ -0,0 +1,125 @@ +// +using System; +using IdentityServer4.EntityFramework.DbContexts; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +namespace Dough.Migrations.IdentityServer.PersistedGrant +{ + [DbContext(typeof(PersistedGrantDbContext))] + partial class PersistedGrantDbContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "3.1.6") + .HasAnnotation("Relational:MaxIdentifierLength", 64); + + modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.DeviceFlowCodes", b => + { + b.Property("UserCode") + .HasColumnType("varchar(200) CHARACTER SET utf8mb4") + .HasMaxLength(200); + + b.Property("ClientId") + .IsRequired() + .HasColumnType("varchar(200) CHARACTER SET utf8mb4") + .HasMaxLength(200); + + b.Property("CreationTime") + .HasColumnType("datetime(6)"); + + b.Property("Data") + .IsRequired() + .HasColumnType("longtext CHARACTER SET utf8mb4") + .HasMaxLength(50000); + + b.Property("Description") + .HasColumnType("varchar(200) CHARACTER SET utf8mb4") + .HasMaxLength(200); + + b.Property("DeviceCode") + .IsRequired() + .HasColumnType("varchar(200) CHARACTER SET utf8mb4") + .HasMaxLength(200); + + b.Property("Expiration") + .IsRequired() + .HasColumnType("datetime(6)"); + + b.Property("SessionId") + .HasColumnType("varchar(100) CHARACTER SET utf8mb4") + .HasMaxLength(100); + + b.Property("SubjectId") + .HasColumnType("varchar(200) CHARACTER SET utf8mb4") + .HasMaxLength(200); + + b.HasKey("UserCode"); + + b.HasIndex("DeviceCode") + .IsUnique(); + + b.HasIndex("Expiration"); + + b.ToTable("DeviceCodes"); + }); + + modelBuilder.Entity("IdentityServer4.EntityFramework.Entities.PersistedGrant", b => + { + b.Property("Key") + .HasColumnType("varchar(200) CHARACTER SET utf8mb4") + .HasMaxLength(200); + + b.Property("ClientId") + .IsRequired() + .HasColumnType("varchar(200) CHARACTER SET utf8mb4") + .HasMaxLength(200); + + b.Property("ConsumedTime") + .HasColumnType("datetime(6)"); + + b.Property("CreationTime") + .HasColumnType("datetime(6)"); + + b.Property("Data") + .IsRequired() + .HasColumnType("longtext CHARACTER SET utf8mb4") + .HasMaxLength(50000); + + b.Property("Description") + .HasColumnType("varchar(200) CHARACTER SET utf8mb4") + .HasMaxLength(200); + + b.Property("Expiration") + .HasColumnType("datetime(6)"); + + b.Property("SessionId") + .HasColumnType("varchar(100) CHARACTER SET utf8mb4") + .HasMaxLength(100); + + b.Property("SubjectId") + .HasColumnType("varchar(200) CHARACTER SET utf8mb4") + .HasMaxLength(200); + + b.Property("Type") + .IsRequired() + .HasColumnType("varchar(50) CHARACTER SET utf8mb4") + .HasMaxLength(50); + + b.HasKey("Key"); + + b.HasIndex("Expiration"); + + b.HasIndex("SubjectId", "ClientId", "Type"); + + b.HasIndex("SubjectId", "SessionId", "Type"); + + b.ToTable("PersistedGrants"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/server/Models/Constants.cs b/src/server/Models/Constants.cs index 1e49323..564bfb2 100644 --- a/src/server/Models/Constants.cs +++ b/src/server/Models/Constants.cs @@ -1,15 +1,7 @@ -using System.Collections.Generic; -using System.Security.Claims; - namespace Dough.Models { public static class Constants { - public static class ClaimNames - { - public static string Id = "sub"; - } - public static readonly string[] BrowserAppUrls = { "http://localhost:8080", diff --git a/src/server/Program.cs b/src/server/Program.cs index 04a9447..5c01dac 100644 --- a/src/server/Program.cs +++ b/src/server/Program.cs @@ -1,4 +1,5 @@ using System; +using System.IO; using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.Hosting; using Serilog; @@ -19,7 +20,7 @@ namespace Dough .MinimumLevel.Override("Microsoft.AspNetCore.Authentication", LogEventLevel.Information) .Enrich.FromLogContext() .WriteTo.File( - @"/var/log/money-manager.txt", + Path.Combine(Directory.GetCurrentDirectory(), "AppData", "logs", DateTime.Today.ToShortDateString()), fileSizeLimitBytes: 4_000_000, rollOnFileSizeLimit: true, shared: true, @@ -44,7 +45,7 @@ namespace Dough } } - public static IHostBuilder CreateHostBuilder(string[] args) => + private static IHostBuilder CreateHostBuilder(string[] args) => Host.CreateDefaultBuilder(args) .UseSerilog() .ConfigureWebHostDefaults(webBuilder => diff --git a/src/server/Startup.cs b/src/server/Startup.cs index 7ebe86b..abc305d 100644 --- a/src/server/Startup.cs +++ b/src/server/Startup.cs @@ -1,4 +1,5 @@ using System.IO; +using System.Reflection; using System.Security.Cryptography.X509Certificates; using Dough.IdentityServer; using Microsoft.AspNetCore.Builder; @@ -11,34 +12,37 @@ using Dough.Models; using Dough.Models.Database; using Dough.Services; using IdentityServer4.Configuration; +using IdentityServer4.EntityFramework.DbContexts; using Microsoft.AspNetCore.DataProtection; +using Microsoft.AspNetCore.Diagnostics.HealthChecks; namespace Dough { public class Startup { - public Startup(IConfiguration configuration) - { - Configuration = configuration; - } - - public IConfiguration Configuration { get; } + private IConfiguration _configuration { get; } + private IWebHostEnvironment _environment { get; } private const string DefaultCorsPolicy = "DefaultCorsPolicy"; - private string GetConnectionStringFromEnvironment() + public Startup(IConfiguration configuration, IWebHostEnvironment environment) { - var host = Configuration.GetValue("DB_HOST"); - var port = Configuration.GetValue("DB_PORT"); - var user = Configuration.GetValue("DB_USER"); - var password = Configuration.GetValue("DB_PASSWORD"); - var name = Configuration.GetValue("DB_NAME"); - return $"Server={host},{port};Database={name};User={user};Password={password}"; + _configuration = configuration; + _environment = environment; } - private X509Certificate2 GetSigningCredentialFromPfx(string fileName) + private string GetConnectionStringFromEnvironment(string database) { - var path = Path.Combine(Directory.GetCurrentDirectory(), "AppData", fileName); + var host = _configuration.GetValue("DB_HOST"); + var port = _configuration.GetValue("DB_PORT"); + var user = _configuration.GetValue("DB_USER"); + var password = _configuration.GetValue("DB_PASSWORD"); + return $"Server={host},{port};Database={database};User={user};Password={password}"; + } + + private static X509Certificate2 GetSigningCredentialFromPfx(string fileName) + { + var path = Path.Combine(Directory.GetCurrentDirectory(), "AppData", "certs", fileName); return new X509Certificate2(path, string.Empty); } @@ -63,9 +67,19 @@ namespace Dough services.AddDbContext(options => { - options.UseMySql(GetConnectionStringFromEnvironment()); + options.UseMySql(GetConnectionStringFromEnvironment("dough"), + builder => { builder.EnableRetryOnFailure(); } + ); + if (_environment.IsDevelopment()) + { + options.EnableSensitiveDataLogging(); + } }); + var migrationsAssembly = typeof(Startup).GetTypeInfo().Assembly.GetName().Name; + + services.AddAuthentication().AddLocalApi(); + services.AddIdentityServer(options => { options.UserInteraction = new UserInteractionOptions @@ -74,33 +88,48 @@ namespace Dough ErrorUrl = "/error", }; }) + .AddOperationalStore(options => + { + options.ConfigureDbContext = builder => + { + builder.UseMySql(GetConnectionStringFromEnvironment("dough_tokens"), + sql => + { + sql.MigrationsAssembly(migrationsAssembly); + sql.EnableRetryOnFailure(); + }); + if (_environment.IsDevelopment()) + { + builder.EnableSensitiveDataLogging(); + } + }; + }) .AddInMemoryIdentityResources(Config.IdentityResources) .AddInMemoryApiScopes(Config.ApiScopes) + .AddInMemoryClients(Config.Clients) .AddSigningCredential(GetSigningCredentialFromPfx("example.pfx")) - .AddValidationKey(GetSigningCredentialFromPfx("example2.pfx")) - .AddProfileService() - .AddInMemoryClients(Config.Clients); - + .AddProfileService(); + services.AddSingleton(); services.AddControllers(); services.AddRazorPages().AddRazorRuntimeCompilation(); } - public void Configure(IApplicationBuilder app, IWebHostEnvironment env) + public void Configure(IApplicationBuilder app) { - if (env.IsDevelopment()) + if (_environment.IsDevelopment()) app.UseDeveloperExceptionPage(); app.UseRouting(); app.UseStaticFiles(); app.UseCors(DefaultCorsPolicy); - app.UseHealthChecks("/health"); app.UseStatusCodePages(); app.UseIdentityServer(); app.UseAuthorization(); app.UseEndpoints(endpoints => { + endpoints.MapHealthChecks("/health"); endpoints.MapRazorPages(); endpoints.MapControllers() .RequireAuthorization(); diff --git a/src/server/appsettings.json b/src/server/appsettings.json deleted file mode 100644 index 81ff877..0000000 --- a/src/server/appsettings.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "Logging": { - "LogLevel": { - "Default": "Information", - "Microsoft": "Warning", - "Microsoft.Hosting.Lifetime": "Information" - } - }, - "AllowedHosts": "*" -} diff --git a/src/server/wwwroot/login.js b/src/server/wwwroot/login.js index 6f80a3f..8f2bd73 100644 --- a/src/server/wwwroot/login.js +++ b/src/server/wwwroot/login.js @@ -4,7 +4,7 @@ let titleEl = document.querySelector("#title"); let messageEl = document.querySelector("#message"); const loginBtn = document.querySelector("#login-btn"); let returnUrl = new URL(location.href).searchParams.get("ReturnUrl"); -if(!returnUrl) location.href = "http://localhost:3000"; +if (!returnUrl) location.href = "http://localhost:3000"; form.addEventListener("submit", () => { const username = document.querySelector("#username").value; const password = document.querySelector("#password").value; @@ -41,7 +41,7 @@ form.addEventListener("submit", () => { location.href = returnUrl; } }) - .catch((error) => console.error(error)); + .catch((error) => error(error)); }); function displayError(title, message) { @@ -70,4 +70,4 @@ function initAlertEvent(element) { event.preventDefault(); Util.removeClass(element.closest(".js-alert"), "alert--is-visible"); }); -} \ No newline at end of file +} -- cgit v1.3