diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..faa5ada --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2021 Ludovic, Jean, Michel Jarno + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..547de5d --- /dev/null +++ b/README.md @@ -0,0 +1,184 @@ +# All-In-One CRUD + +```bash +git clone https://github.com/ludojmj/svelte-netcore-identity.git +``` + +The aim of this project is to gather, in a single place, useful front and back ends development tools: + +- A Database with SQLite; +- A Web API server with .NET Core 5.x; +- A Svelte JS client App; +- A link to an external service for identity management, authorization, and API security. + +## Quick start (Development) + +Server + +```bash +cd /Server +dotnet run +``` + +Client + +```bash +cd /client +npm install +npm run dev +``` + +--- + +## Inspiration + +- SQLite database powered by: +- Server based on API mechanisms of: +- Svelte template client borrowed from: +- Identity service powered by: +- Identity client borrowed from: +- CSS borrowed from: +- SVG borrowed from: + +--- + +## Manufacturing process steps + +### >>>>> SQLite database + +#### Overwrite database if needed + +```bash +cd +sqlite3 Server/App_Data/stuff.db < Server/App_Data/create_tables.sql +``` + +### >>>>> .NET Core 5.x Web API server + +#### Create the server project + +```bash +cd +dotnet new gitignore +dotnet new webapi -n Server +dotnet new xunit -n Server.UnitTest +``` + +#### Generate the model from the database for the Web API server + +```bash +dotnet tool install --global dotnet-ef +cd /Server +dotnet ef dbcontext scaffold "Data Source=App_Data/stuff.db" Microsoft.EntityFrameworkCore.Sqlite \ +--output-dir DbModels --context-dir DbModels --context StuffDbContext --force +``` + +#### Run the tests + +```bash +cd /Server.UnitTest +dotnet restore +dotnet build +dotnet test +dotnet test /p:CollectCoverage=true +``` + +#### Run the Web API server + +```bash +cd /Server +export ASPNETCORE_ENVIRONMENT=Development +dotnet run +``` + +### >>>>> Svelte client App + +#### Create the client project + +```bash +cd +git clone https://github.com/sveltejs/template.git +mv template myApp +``` + +#### Run the client App + +```bash +cd /client +npm install +npm run dev +``` + +#### Possibly run a standalone version of the client App (mocking) except identity server + +At the second line of the file: + + > ```/client/src/api/stuff.js``` + +Swap isProd to !isProd: + + > const rootApi = !isProd ? "https://localhost:5001/api/stuff" : "http://localhost:3000/mock/stuff"; + +--- + +## Troubleshooting + +### _An error occured. Please try again later._ + +**When?** + +- Creating a record in the SQLite database _stuff.db_ running Linux on Azure; +- The "real" error (not displayed in Production) is: _SQLite Error 5: 'database is locked'_; +- There is a restricted write access to the file on Linux web app when running on Azure. + +**How to solve:** + +- ==> Either use a real database or deploy the web app on Azure choosing Windows OS. + +### _SQLite Error 1: 'no such table: t_stuff'_ + +**When?** + +- Running the Svelte client App (```npm run dev```); +- Connecting to: . + +**How to solve:** + +- ==> Create the database _stuff.db_ (```sqlite3 Server/App_Data/stuff.db < Server/App_Data/create_tables.sql```). + +### _Network Error_ + +**When?** + +- Running the Svelte client App (```npm run dev```); +- Connecting to: . + +**How to solve:** + +- ==> Start the .NET Core server (```dotnet run```) before the Svelte client App (```npm run dev```). + +### _Your connection is not private_ (NET::ERR_CERT_AUTHORITY_INVALID) + +**When?** + +- Running the .NET Core server (dotnet run); +- Connecting to: ; +- Or connecting to its redirection: . + +**How to solve:** + +- ==> Click "Advanced settings" button; +- ==> Click on the link to continue to the assumed unsafe localhost site; +- ==> Accept self-signed localhost certificate. + +### _You do not have permission to view this directory or page._ + +**When?** + +- Browsing the web site on a Azure Windows instance. + +**How to solve:** + +- ==> Add the web.config file since you've got IIS running; +- ==> On Linux, the web.config file is useless +(Update your http headers according to the suitable Web Server configuration file). diff --git a/Server.UnitTest/Controllers/StuffControllerTest.cs b/Server.UnitTest/Controllers/StuffControllerTest.cs new file mode 100644 index 0000000..e3f80f3 --- /dev/null +++ b/Server.UnitTest/Controllers/StuffControllerTest.cs @@ -0,0 +1,168 @@ +using System.Collections.ObjectModel; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; +using Moq; +using Xunit; +using Server.Controllers; +using Server.Models; +using Server.Repository.Interfaces; + +namespace Server.UnitTest.Controllers +{ + public class StuffControllerTest + { + private static readonly UserModel CurrentUserModelTest = new UserModel() + { + Id = "11", + Name = "GivenName FamilyName", + GivenName = "GivenName", + FamilyName = "FamilyName", + Email = "Email" + }; + + private static readonly DatumModel TestDatum = new DatumModel + { + Id = "1", + Label = "Label", + Description = "Description", + OtherInfo = "OtherInfo", + User = CurrentUserModelTest + }; + + private static readonly StuffModel TestStuff = new StuffModel + { + DatumList = new Collection + { + TestDatum + } + }; + + // ***** ***** ***** LIST + [Fact] + public async Task StuffController_GetStuffList_ShouldReturn_Ok() + { + // Arrange + var mockStuffRepo = Mock.Of(x => x.GetListAsync(1) == Task.FromResult(TestStuff)); + var controller = new StuffController(mockStuffRepo); + int existingPage = 1; + + // Act + IActionResult actionResult = await controller.GetList(existingPage, null); + + // Assert + var okResult = Assert.IsType(actionResult); + var contentResult = Assert.IsType(okResult.Value); + var expected = TestDatum.Id; + var actual = contentResult.DatumList.ToArray()[0].Id; + Assert.Equal(expected, actual); + } + + // ***** ***** ***** SEARCH + [Fact] + public async Task StuffController_SearchStuffList_ShouldReturn_Ok() + { + // Arrange + var mockStuffRepo = Mock.Of(x => x.SearchListAsync("foo") == Task.FromResult(TestStuff)); + var controller = new StuffController(mockStuffRepo); + + // Act + IActionResult actionResult = await controller.GetList(0, "foo"); + + // Assert + var okResult = Assert.IsType(actionResult); + var contentResult = Assert.IsType(okResult.Value); + var expected = TestDatum.Id; + var actual = contentResult.DatumList.ToArray()[0].Id; + Assert.Equal(expected, actual); + } + + // ***** ***** ***** CREATE + [Fact] + public async Task StuffController_Create_ShouldReturnCreated() + { + // Arrange + var mockStuffRepo = Mock.Of(x => x.CreateAsync(TestDatum) == Task.FromResult(TestDatum)); + var controller = new StuffController(mockStuffRepo); + + // Act + IActionResult actionResult = await controller.Create(TestDatum); + + // Assert + var okResult = Assert.IsType(actionResult); + var contentResult = Assert.IsType(okResult.Value); + Assert.Equal(TestDatum.Id, contentResult.Id); + } + + // ***** ***** ***** READ SINGLE + [Fact] + public async Task StuffController_Read_ShouldReturn_Ok() + { + // Arrange + var mockStuffRepo = Mock.Of(x => x.ReadAsync("1") == Task.FromResult(TestDatum)); + var controller = new StuffController(mockStuffRepo); + + // Act + IActionResult actionResult = await controller.Read("1"); + + // Assert + var okResult = Assert.IsType(actionResult); + var contentResult = Assert.IsType(okResult.Value); + var expected = TestDatum.Id; + var actual = contentResult.Id; + Assert.Equal(expected, actual); + } + + [Fact] + public async Task StuffController_Read_ShouldReturn_Null() + { + // Arrange + var mockUserRepo = Mock.Of(); + var controller = new StuffController(mockUserRepo); + + // Act + IActionResult actionResult = await controller.Read("1"); + + // Assert + var okResult = Assert.IsType(actionResult); + var contentResult = okResult.Value; + StuffModel expected = null; + var actual = contentResult; + Assert.Equal(expected, actual); + } + + // ***** ***** ***** UPDATE + [Fact] + public async Task StuffController_UpdateStuff_ShouldReturn_Ok() + { + // Arrange + var mockStuffRepo = Mock.Of(x => x.UpdateAsync("1", TestDatum) == Task.FromResult(TestDatum)); + var controller = new StuffController(mockStuffRepo); + string existingId = "1"; + + // Act + IActionResult actionResult = await controller.Update(existingId, TestDatum); + + // Assert + var okResult = Assert.IsType(actionResult); + var contentResult = Assert.IsType(okResult.Value); + Assert.Equal(TestDatum.Id, contentResult.Id); + } + + // ***** ***** ***** DELETE + [Fact] + public async Task StuffController_DeleteStuff_ShouldReturnNoContent() + { + // Arrange + var mockStuffRepo = Mock.Of(); + var controller = new StuffController(mockStuffRepo); + string badId = "2"; + + // Act + IActionResult actionResult = await controller.Delete(badId); + + // Assert + Assert.IsType(actionResult); + } + } +} diff --git a/Server.UnitTest/Controllers/UserControllerTest.cs b/Server.UnitTest/Controllers/UserControllerTest.cs new file mode 100644 index 0000000..0526949 --- /dev/null +++ b/Server.UnitTest/Controllers/UserControllerTest.cs @@ -0,0 +1,159 @@ +using System.Collections.ObjectModel; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; +using Moq; +using Xunit; +using Server.Controllers; +using Server.Models; +using Server.Repository.Interfaces; + +namespace Server.UnitTest.Controllers +{ + public class UserControllerTest + { + private static readonly UserModel TestUser = new UserModel() + { + Id = "11", + Name = "GivenName FamilyName", + GivenName = "GivenName", + FamilyName = "FamilyName", + Email = "Email" + }; + + private static readonly DirectoryModel TestDirectory = new DirectoryModel + { + UserList = new Collection + { + TestUser + } + }; + + // ***** ***** ***** LIST + [Fact] + public async Task UserController_GetUserList_ShouldReturn_Ok() + { + // Arrange + var mockUserRepo = Mock.Of(x => x.GetListAsync(1) == Task.FromResult(TestDirectory)); + var controller = new UserController(mockUserRepo); + int existingPage = 1; + + // Act + IActionResult actionResult = await controller.GetList(existingPage, null); + + // Assert + var okResult = Assert.IsType(actionResult); + var contentResult = Assert.IsType(okResult.Value); + var expected = TestUser.Id; + var actual = contentResult.UserList.ToArray()[0].Id; + Assert.Equal(expected, actual); + } + + // ***** ***** ***** SEARCH + [Fact] + public async Task UserController_SearchUserList_ShouldReturn_Ok() + { + // Arrange + var mockUserRepo = Mock.Of(x => x.SearchListAsync("foo") == Task.FromResult(TestDirectory)); + var controller = new UserController(mockUserRepo); + + // Act + IActionResult actionResult = await controller.GetList(0, "foo"); + + // Assert + var okResult = Assert.IsType(actionResult); + var contentResult = Assert.IsType(okResult.Value); + var expected = TestUser.Id; + var actual = contentResult.UserList.ToArray()[0].Id; + Assert.Equal(expected, actual); + } + + // ***** ***** ***** CREATE + [Fact] + public async Task UserController_Create_ShouldReturnCreated() + { + // Arrange + var mockUserRepo = Mock.Of(x => x.CreateAsync(TestUser) == Task.FromResult(TestUser)); + var controller = new UserController(mockUserRepo); + + // Act + IActionResult actionResult = await controller.Create(TestUser); + + // Assert + var okResult = Assert.IsType(actionResult); + var contentResult = Assert.IsType(okResult.Value); + Assert.Equal(TestUser.Id, contentResult.Id); + } + + // ***** ***** ***** READ SINGLE + [Fact] + public async Task UserController_Read_ShouldReturn_Ok() + { + // Arrange + var mockUserRepo = Mock.Of(x => x.ReadAsync("1") == Task.FromResult(TestUser)); + var controller = new UserController(mockUserRepo); + + // Act + IActionResult actionResult = await controller.Read("1"); + + // Assert + var okResult = Assert.IsType(actionResult); + var contentResult = Assert.IsType(okResult.Value); + var expected = TestUser.Id; + var actual = contentResult.Id; + Assert.Equal(expected, actual); + } + + [Fact] + public async Task UserController_Read_ShouldReturn_Null() + { + // Arrange + var mockUserRepo = Mock.Of(); + var controller = new UserController(mockUserRepo); + + // Act + IActionResult actionResult = await controller.Read("1"); + + // Assert + var okResult = Assert.IsType(actionResult); + var contentResult = okResult.Value; + UserModel expected = null; + var actual = contentResult; + Assert.Equal(expected, actual); + } + + // ***** ***** ***** UPDATE + [Fact] + public async Task UserController_UpdateUser_ShouldReturn_Ok() + { + // Arrange + var mockUserRepo = Mock.Of(x => x.UpdateAsync("1", TestUser) == Task.FromResult(TestUser)); + var controller = new UserController(mockUserRepo); + string existingId = "1"; + + // Act + IActionResult actionResult = await controller.Update(existingId, TestUser); + + // Assert + var okResult = Assert.IsType(actionResult); + var contentResult = Assert.IsType(okResult.Value); + Assert.Equal(TestUser.Id, contentResult.Id); + } + + // ***** ***** ***** DELETE + [Fact] + public async Task UserController_DeleteUser_ShouldReturnNoContent() + { + // Arrange + var mockUserRepo = Mock.Of(); + var controller = new UserController(mockUserRepo); + string badId = "2"; + + // Act + IActionResult actionResult = await controller.Delete(badId); + + // Assert + Assert.IsType(actionResult); + } + } +} diff --git a/Server.UnitTest/Repository/StuffRepositoryTest.cs b/Server.UnitTest/Repository/StuffRepositoryTest.cs new file mode 100644 index 0000000..19563d2 --- /dev/null +++ b/Server.UnitTest/Repository/StuffRepositoryTest.cs @@ -0,0 +1,392 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.Data.Sqlite; +using Microsoft.EntityFrameworkCore; +using Moq; +using Xunit; +using Server.DbModels; +using Server.Models; +using Server.Repository; +using Server.Repository.Interfaces; +using Server.Shared; + +namespace Server.UnitTest.Repository +{ + public class StuffRepositoryTest + { + private static readonly UserModel UserModelTest = new UserModel() + { + Id = "11", + Name = "GivenName FamilyName", + GivenName = "GivenName", + FamilyName = "FamilyName", + Email = "Email" + }; + + private static readonly DatumModel DatumModelTest = new DatumModel + { + Id = "1", + Label = "Label", + Description = "Description", + OtherInfo = "OtherInfo", + User = UserModelTest + }; + + private readonly TUser _dbUser = new TUser + { + UsrId = "11", + UsrName = "GivenName FamilyName", + UsrGivenName = "GivenName", + UsrFamilyName = "FamilyName", + UsrEmail = "Email" + }; + + private readonly TStuff _dbStuff = new TStuff + { + StfId = "1", + StfUserId = "11", + StfLabel = "Label", + StfDescription = "Description", + StfOtherInfo = "OtherInfo", + StfCreatedAt = DateTime.UtcNow.ToString("o") + }; + + private readonly SqliteConnection _connection; + private readonly StuffDbContext _context; + private readonly IStuffRepo _stuffRepo; + + public StuffRepositoryTest() + { + _connection = new SqliteConnection("DataSource=:memory:"); + _connection.Open(); + var options = new DbContextOptionsBuilder() + .UseSqlite(_connection) + .Options; + _context = new StuffDbContext(options); + _context.Database.EnsureCreated(); + var mockAuth = Mock.Of(x => x.GetCurrentUserAsync(It.IsAny()) == Task.FromResult(_dbUser)); + _stuffRepo = new StuffRepo(_context, mockAuth); + } + + [Fact] + public void Dispose() + { + _context.Dispose(); + _connection.Close(); + } + + // ***** ***** ***** LIST + [Fact] + public async Task StuffRepo_GetListAsync_ShouldReturn_Ok() + { + // Arrange + _context.Add(_dbUser); + _context.Add(_dbStuff); + _context.SaveChanges(); + + // Act + var repoResult = await _stuffRepo.GetListAsync(1); + + // Assert + var expected = DatumModelTest.Id; + var actual = repoResult.DatumList.ToArray()[0].Id; + Assert.Equal(expected, actual); + } + + [Fact] + public async Task StuffRepo_GetListAsync_ShouldReturn_PageOne() + { + // Arrange + int requestedPage = 2; + _context.Add(_dbUser); + _context.Add(_dbStuff); + _context.SaveChanges(); + + // Act + var repoResult = await _stuffRepo.GetListAsync(requestedPage); + + // Assert + var expected = 1; + var actual = repoResult.Page; + Assert.Equal(expected, actual); + } + + // ***** ***** ***** SEARCH + [Fact] + public async Task StuffRepo_SearchListAsync_ShouldReturn_Ok() + { + // Arrange + _context.Add(_dbUser); + _context.Add(_dbStuff); + _context.SaveChanges(); + + // Act + var repoResult = await _stuffRepo.SearchListAsync("LABEL"); + + // Assert + var expected = DatumModelTest.Id; + var actual = repoResult.DatumList.ToArray()[0].Id; + Assert.Equal(expected, actual); + } + + [Fact] + public async Task StuffRepo_SearchListAsync_ShouldThrow_ArgumentException() + { + // Arrange + _context.Add(_dbUser); + var dbStuffList = new List(); + for (int idx = 0; idx < 7; idx++) + { + var tmpStuff = new TStuff + { + StfId = _dbStuff.StfId + (idx + 1), StfUserId = _dbStuff.StfUserId, StfLabel = _dbStuff.StfLabel + }; + dbStuffList.Add(tmpStuff); + } + + _context.AddRange(dbStuffList); + _context.SaveChanges(); + + // Act + var repoResult = _stuffRepo.SearchListAsync("LABEL"); + var exception = await Record.ExceptionAsync(() => repoResult); + + // Assert + Assert.NotNull(exception); + Assert.IsType(exception); + Assert.Equal("Too many results. Please narrow your search.", exception.Message); + } + + // ***** ***** ***** CREATE + [Fact] + public async Task StuffRepo_CreateAsync_ShouldReturn_Ok() + { + // Arrange1 + // Existing user + _context.Add(_dbUser); + _context.SaveChanges(); + + // Act1 + var repoResult = await _stuffRepo.CreateAsync(DatumModelTest); + + // Assert1 + int actual = repoResult.Id.Count(x => x == '-'); + int expected = 4; + Assert.Equal(expected, actual); + + // *** + // Arrange2 + // Creating user at the same time as stuff + DatumModelTest.User = null; + + // Act2 + repoResult = await _stuffRepo.CreateAsync(DatumModelTest); + + // Assert2 + actual = repoResult.Id.Count(x => x == '-'); + expected = 4; + Assert.Equal(expected, actual); + + // Restore + DatumModelTest.User = UserModelTest; + } + + [Fact] + public async Task StuffRepo_CreateAsync_ShouldReturn_ArgumentException() + { + // Arrange + DatumModelTest.Label = string.Empty; + _context.Add(_dbUser); + _context.SaveChanges(); + + // Act + var repoResult = _stuffRepo.CreateAsync(DatumModelTest); + var exception = await Record.ExceptionAsync(() => repoResult); + + // Assert + Assert.NotNull(exception); + Assert.IsType(exception); + Assert.Equal("The label cannot be empty.", exception.Message); + + // Restore + DatumModelTest.Label = "Label"; + } + + // ***** ***** ***** READ SINGLE + [Fact] + public async Task StuffRepo_ReadAsync_ShouldReturn_Ok() + { + // Arrange + _context.Add(_dbUser); + _context.Add(_dbStuff); + _context.SaveChanges(); + + // Act + var repoResult = await _stuffRepo.ReadAsync("1"); + + // Assert + var expected = DatumModelTest.Id; + var actual = repoResult.Id; + Assert.Equal(expected, actual); + } + + [Fact] + public async Task StuffRepo_ReadAsync_ShouldThrow_NotFoundException() + { + // Arrange + // No stuff + + // Act + var repoResult = _stuffRepo.ReadAsync("2"); + var exception = await Record.ExceptionAsync(() => repoResult); + + // Assert + Assert.NotNull(exception); + Assert.IsType(exception); + Assert.Equal("Stuff not found.", exception.Message); + } + + // ***** ***** ***** UPDATE + [Fact] + public async Task StuffRepo_UpdateAsync_ShouldReturn_Ok() + { + // Arrange + _context.Add(_dbUser); + _context.Add(_dbStuff); + _context.SaveChanges(); + + // Act + var repoResult = await _stuffRepo.UpdateAsync("1", DatumModelTest); + + // Assert + var expected = DatumModelTest.Id; + var actual = repoResult.Id; + Assert.Equal(expected, actual); + } + + [Fact] + public async Task StuffRepo_UpdateAsync_ShouldThrow_ArgumentException() + { + // Arrange1 + // _datumModelTest.Id != input.Id + + // Act1 + var repoResult = _stuffRepo.UpdateAsync("2", DatumModelTest); + var exception = await Record.ExceptionAsync(() => repoResult); + + // Assert1 + Assert.NotNull(exception); + Assert.IsType(exception); + Assert.Equal("Corrupted data.", exception.Message); + + // *** + // Arrange2 + DatumModelTest.Id = "StuffNotFound"; + + // Act2 + repoResult = _stuffRepo.UpdateAsync("StuffNotFound", DatumModelTest); + exception = await Record.ExceptionAsync(() => repoResult); + + // Assert2 + Assert.NotNull(exception); + Assert.IsType(exception); + Assert.Equal("Corrupted data.", exception.Message); + // Restore + DatumModelTest.Id = "1"; + + // *** + // Arrange3 + DatumModelTest.Label = string.Empty; + + // Act3 + repoResult = _stuffRepo.UpdateAsync(DatumModelTest.Id, DatumModelTest); + exception = await Record.ExceptionAsync(() => repoResult); + + // Assert3 + Assert.NotNull(exception); + Assert.IsType(exception); + Assert.Equal("The label cannot be empty.", exception.Message); + // Restore + DatumModelTest.Label = "Label"; + + // *** + // Arrange4 + _dbUser.UsrId = "2"; + _dbStuff.StfUserId = "2"; + _context.Add(_dbUser); + _context.Add(_dbStuff); + _context.SaveChanges(); + _dbUser.UsrId = "11"; + + // Act4 + repoResult = _stuffRepo.UpdateAsync("1", DatumModelTest); + exception = await Record.ExceptionAsync(() => repoResult); + + // Assert4 + Assert.NotNull(exception); + Assert.IsType(exception); + Assert.Equal("Corrupted data.", exception.Message); + + // Restore + _dbStuff.StfUserId = "11"; + _dbStuff.StfUser.UsrId = "11"; + } + + // ***** ***** ***** DELETE + [Fact] + public async Task StuffRepo_DeleteAsync_ShouldReturn_Ok() + { + // Arrange + _context.Add(_dbUser); + _context.Add(_dbStuff); + _context.SaveChanges(); + + // Act + await _stuffRepo.DeleteAsync("1"); + var actual = _context.TStuff.FirstOrDefault(x => x.StfId.Equals("1")); + + // Assert + Assert.Null(actual); + } + + [Fact] + public async Task StuffRepo_DeleteAsync_ShouldThrow_ArgumentException() + { + // Arrange1 + // No stuff + + // Act1 + var repoResult = _stuffRepo.DeleteAsync("2"); + var exception = await Record.ExceptionAsync(() => repoResult); + + // Assert1 + Assert.NotNull(exception); + Assert.IsType(exception); + Assert.Equal("Corrupted data.", exception.Message); + + // *** + // Arrange2 + _dbUser.UsrId = "2"; + _dbStuff.StfUserId = "2"; + _context.Add(_dbUser); + _context.Add(_dbStuff); + _context.SaveChanges(); + _dbUser.UsrId = "11"; + + // Act2 + repoResult = _stuffRepo.DeleteAsync("1"); + exception = await Record.ExceptionAsync(() => repoResult); + + // Assert2 + Assert.NotNull(exception); + Assert.IsType(exception); + Assert.Equal("Corrupted data.", exception.Message); + + // Restore + _dbStuff.StfUserId = "11"; + _dbStuff.StfUser.UsrId = "11"; + } + } +} diff --git a/Server.UnitTest/Repository/UserRepositoryTest.cs b/Server.UnitTest/Repository/UserRepositoryTest.cs new file mode 100644 index 0000000..975667a --- /dev/null +++ b/Server.UnitTest/Repository/UserRepositoryTest.cs @@ -0,0 +1,410 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.Data.Sqlite; +using Microsoft.EntityFrameworkCore; +using Moq; +using Xunit; +using Server.DbModels; +using Server.Models; +using Server.Repository; +using Server.Repository.Interfaces; + +namespace Server.UnitTest.Repository +{ + public class UserRepositoryTest + { + private static readonly UserModel UserModelTest = new UserModel() + { + Id = "11", + Name = "GivenName FamilyName", + GivenName = "GivenName", + FamilyName = "FamilyName", + Email = "Email", + CreatedAt = DateTime.UtcNow, + UpdatedAt = DateTime.UtcNow + }; + + private readonly TUser _dbUser = new TUser + { + UsrId = "11", + UsrName = "GivenName FamilyName", + UsrGivenName = "GivenName", + UsrFamilyName = "FamilyName", + UsrEmail = "Email", + UsrCreatedAt = DateTime.UtcNow.ToString("o"), + UsrUpdatedAt = DateTime.UtcNow.ToString("o") + }; + + private readonly SqliteConnection _connection; + private readonly StuffDbContext _context; + private readonly IUserRepo _userRepo; + + public UserRepositoryTest() + { + _connection = new SqliteConnection("DataSource=:memory:"); + _connection.Open(); + var options = new DbContextOptionsBuilder() + .UseSqlite(_connection) + .Options; + _context = new StuffDbContext(options); + _context.Database.EnsureCreated(); + + var mockAuth = Mock.Of(x => x.GetCurrentUserAsync(It.IsAny()) == Task.FromResult(_dbUser)); + _userRepo = new UserRepo(_context, mockAuth); + } + + [Fact] + public void Dispose() + { + _context.Dispose(); + _connection.Close(); + } + + // ***** ***** ***** LIST + [Fact] + public async Task UserRepo_GetListAsync_ShouldReturn_Ok() + { + // Arrange + _context.Add(_dbUser); + _context.SaveChanges(); + + // Act + var repoResult = await _userRepo.GetListAsync(1); + + // Assert + var expected = UserModelTest.Id; + var actual = repoResult.UserList.ToArray()[0].Id; + Assert.Equal(expected, actual); + } + + // ***** ***** ***** SEARCH + [Fact] + public async Task UserRepo_SearchListAsync_ShouldReturn_Ok() + { + // Arrange + _context.Add(_dbUser); + _context.SaveChanges(); + + // Act + var repoResult = await _userRepo.SearchListAsync("GIVENNAME"); + + // Assert + var expected = UserModelTest.Id; + var actual = repoResult.UserList.ToArray()[0].Id; + Assert.Equal(expected, actual); + } + + [Fact] + public async Task UserRepo_SearchListAsync_ShouldThrow_ArgumentException() + { + // Arrange + var dbUserList = new List(); + for (int idx = 0; idx < 7; idx++) + { + var tpmUser = new TUser { UsrId = _dbUser.UsrId + idx, UsrGivenName = _dbUser.UsrGivenName + idx }; + dbUserList.Add(tpmUser); + } + + _context.AddRange(dbUserList); + _context.SaveChanges(); + + // Act + var repoResult = _userRepo.SearchListAsync("GIVENNAME"); + var exception = await Record.ExceptionAsync(() => repoResult); + + // Assert + Assert.NotNull(exception); + Assert.IsType(exception); + Assert.Equal("Too many results. Please narrow your search.", exception.Message); + } + + // ***** ***** ***** CREATE + [Fact] + public async Task UserRepo_CreateAsync_ShouldReturn_Ok() + { + // Arrange + // No user + + // Act + var repoResult = await _userRepo.CreateAsync(UserModelTest); + + // Assert + var expected = UserModelTest.Id; + var actual = repoResult.Id; + Assert.Equal(expected, actual); + } + + [Fact] + public async Task UserRepo_CreateAsync_ShouldThrow_InvalidOperationException() + { + // Arrange + _context.Add(_dbUser); + _context.SaveChanges(); + + // Act + var repoResult = _userRepo.CreateAsync(UserModelTest); + var exception = await Record.ExceptionAsync(() => repoResult); + + // Assert + Assert.NotNull(exception); + Assert.IsType(exception); + // Id null + Assert.Contains("The instance of entity type", exception.Message); + } + + [Fact] + public async Task UserRepo_CreateAsync_ShouldReturn_ArgumentException() + { + // Arrange1 + UserModelTest.Id = string.Empty; + + // Act1 + var repoResult = _userRepo.CreateAsync(UserModelTest); + var exception = await Record.ExceptionAsync(() => repoResult); + + // Assert1 + Assert.NotNull(exception); + Assert.IsType(exception); + Assert.Equal("The id cannot be empty.", exception.Message); + // Restore + UserModelTest.Id = "11"; + + // *** + // Arrange2 + UserModelTest.GivenName = string.Empty; + + // Act2 + repoResult = _userRepo.CreateAsync(UserModelTest); + exception = await Record.ExceptionAsync(() => repoResult); + + // Assert2 + Assert.NotNull(exception); + Assert.IsType(exception); + Assert.Equal("The given name cannot be empty.", exception.Message); + // Restore + UserModelTest.GivenName = "GivenName"; + + // *** + // Arrange3 + UserModelTest.FamilyName = string.Empty; + + // Act3 + repoResult = _userRepo.CreateAsync(UserModelTest); + exception = await Record.ExceptionAsync(() => repoResult); + + // Assert3 + Assert.NotNull(exception); + Assert.IsType(exception); + Assert.Equal("The family name cannot be empty.", exception.Message); + // Restore + UserModelTest.FamilyName = "FamilyName"; + + // *** + // Arrange4 + UserModelTest.Email = string.Empty; + + // Act4 + repoResult = _userRepo.CreateAsync(UserModelTest); + exception = await Record.ExceptionAsync(() => repoResult); + + // Assert4 + Assert.NotNull(exception); + Assert.IsType(exception); + Assert.Equal("The email cannot be empty.", exception.Message); + // Restore + UserModelTest.Email = "Email"; + } + + // ***** ***** ***** READ SINGLE + [Fact] + public async Task UserRepo_ReadAsync_ShouldReturn_Ok() + { + // Arrange + _context.Add(_dbUser); + _context.SaveChanges(); + + // Act + var repoResult = await _userRepo.ReadAsync("11"); + + // Assert + var expected = UserModelTest.Id; + var actual = repoResult.Id; + Assert.Equal(expected, actual); + } + + [Fact] + public async Task UserRepo_ReadAsync_ShouldReturnNull() + { + // Arrange + // No user + + // Act + var repoResult = await _userRepo.ReadAsync("Unknown"); + + // Assert + UserModel expected = null; + var actual = repoResult; + Assert.Equal(expected, actual); + } + + + // ***** ***** ***** UPDATE + [Fact] + public async Task UserRepo_UpdateAsync_ShouldReturn_Ok() + { + // Arrange + _context.Add(_dbUser); + _context.SaveChanges(); + + // Act + var repoResult = await _userRepo.UpdateAsync("11", UserModelTest); + + // Assert + var expected = UserModelTest.Id; + var actual = repoResult.Id; + Assert.Equal(expected, actual); + } + + [Fact] + public async Task UserRepo_UpdateAsync_ShouldThrow_UserNotFound() + { + // Arrange + // _userModelTest.Id = "Unknown"; + + // Act + var repoResult = _userRepo.UpdateAsync("11", UserModelTest); + var exception = await Record.ExceptionAsync(() => repoResult); + + // Assert + Assert.NotNull(exception); + Assert.IsType(exception); + Assert.Equal("Corrupted data.", exception.Message); + } + + [Fact] + public async Task UserRepo_UpdateAsync_ShouldThrow_ArgumentException() + { + // Arrange1 + // _userModelTest.Id != input.Id + + // Act1 + var repoResult = _userRepo.UpdateAsync("Fake", UserModelTest); + var exception = await Record.ExceptionAsync(() => repoResult); + + // Assert1 + Assert.NotNull(exception); + Assert.IsType(exception); + Assert.Equal("Corrupted data.", exception.Message); + + // *** + // Arrange2 + UserModelTest.Id = string.Empty; + + // Act2 + repoResult = _userRepo.UpdateAsync(string.Empty, UserModelTest); + exception = await Record.ExceptionAsync(() => repoResult); + + // Assert2 + Assert.NotNull(exception); + Assert.IsType(exception); + Assert.Equal("The id cannot be empty.", exception.Message); + // Restore + UserModelTest.Id = "11"; + + // *** + // Arrange3 + UserModelTest.GivenName = string.Empty; + + // Act3 + repoResult = _userRepo.UpdateAsync(UserModelTest.Id, UserModelTest); + exception = await Record.ExceptionAsync(() => repoResult); + + // Assert3 + Assert.NotNull(exception); + Assert.IsType(exception); + Assert.Equal("The given name cannot be empty.", exception.Message); + // Restore + UserModelTest.GivenName = "GivenName"; + + // *** + // Arrange4 + UserModelTest.FamilyName = string.Empty; + + // Act4 + repoResult = _userRepo.UpdateAsync(UserModelTest.Id, UserModelTest); + exception = await Record.ExceptionAsync(() => repoResult); + + // Assert4 + Assert.NotNull(exception); + Assert.IsType(exception); + Assert.Equal("The family name cannot be empty.", exception.Message); + // Restore + UserModelTest.FamilyName = "FamilyName"; + + // *** + // Arrange5 + UserModelTest.Email = string.Empty; + + // Act5 + repoResult = _userRepo.UpdateAsync(UserModelTest.Id, UserModelTest); + exception = await Record.ExceptionAsync(() => repoResult); + + // Assert5 + Assert.NotNull(exception); + Assert.IsType(exception); + Assert.Equal("The email cannot be empty.", exception.Message); + // Restore + UserModelTest.Email = "Email"; + } + + // ***** ***** ***** DELETE + [Fact] + public async Task UserRepo_DeleteAsync_ShouldReturn_Ok() + { + // Arrange + _context.Add(_dbUser); + _context.SaveChanges(); + + // Act + await _userRepo.DeleteAsync("11"); + var actual = _context.TUser.FirstOrDefault(x => x.UsrId.Equals("1")); + + // Assert + Assert.Null(actual); + } + + [Fact] + public async Task UserRepo_DeleteAsync_ShouldThrow_UserNotFound() + { + // Arrange + // No user + + // Act + var repoResult = _userRepo.DeleteAsync("11"); + var exception = await Record.ExceptionAsync(() => repoResult); + + // Assert + Assert.NotNull(exception); + Assert.IsType(exception); + Assert.Equal("Corrupted data.", exception.Message); + } + + [Fact] + public async Task UserRepo_DeleteAsync_ShouldThrow_ArgumentException() + { + // Arrange + // No user + + // Act + var repoResult = _userRepo.DeleteAsync("Unknown"); + var exception = await Record.ExceptionAsync(() => repoResult); + + // Assert + Assert.NotNull(exception); + Assert.IsType(exception); + Assert.Equal("Corrupted data.", exception.Message); + } + } +} diff --git a/Server.UnitTest/Server.UnitTest.csproj b/Server.UnitTest/Server.UnitTest.csproj new file mode 100644 index 0000000..4158dcd --- /dev/null +++ b/Server.UnitTest/Server.UnitTest.csproj @@ -0,0 +1,21 @@ + + + + net5.0 + + false + + + + + + + + + + + + + + + diff --git a/Server.UnitTest/Shared/ErrorControllerTest.cs b/Server.UnitTest/Shared/ErrorControllerTest.cs new file mode 100644 index 0000000..8881d08 --- /dev/null +++ b/Server.UnitTest/Shared/ErrorControllerTest.cs @@ -0,0 +1,97 @@ +using System; +using Microsoft.AspNetCore.Diagnostics; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; +using Moq; +using Xunit; +using Server.Shared; + +namespace Server.UnitTest.Shared +{ + public class ErrorControllerTest + { + [Fact] + public void ErrorController_NotFoundObjectResult() + { + // Arrange + var mockEnv = Mock.Of(); + var mockLog = Mock.Of>(); + var mockException = Mock.Of(x => x.Error == new NotFoundException("Not found")); + + var context = new DefaultHttpContext(); + context.Features.Set(mockException); + + var controller = new ErrorController(mockEnv, mockLog) + { + ControllerContext = new ControllerContext() { HttpContext = context } + }; + + // Act + IActionResult actionResult = controller.Error(); + + // Assert + var notFoundResult = Assert.IsType(actionResult); + var contentResult = Assert.IsType(notFoundResult.Value); + var expected = "Not found"; + var actual = contentResult.Error; + Assert.Equal(expected, actual); + } + + [Fact] + public void ErrorHandlerFilter_BadRequestObjectResult_Development() + { + // Arrange + var mockEnv = Mock.Of(x => x.EnvironmentName == "Development"); + var mockLog = Mock.Of>(); + var mockException = Mock.Of(x => x.Error == new ArgumentException("Should be displayed")); + + var context = new DefaultHttpContext(); + context.Features.Set(mockException); + + var controller = new ErrorController(mockEnv, mockLog) + { + ControllerContext = new ControllerContext() { HttpContext = context } + }; + + // Act + IActionResult actionResult = controller.Error(); + + // Assert + var notFoundResult = Assert.IsType(actionResult); + var contentResult = Assert.IsType(notFoundResult.Value); + var expected = "Should be displayed"; + var actual = contentResult.Error; + Assert.Equal(expected, actual); + } + + + [Fact] + public void ErrorHandlerFilter_BadRequestObjectResult_Production() + { + // Arrange + var mockEnv = Mock.Of(x => x.EnvironmentName == "Production"); + var mockLog = Mock.Of>(); + var mockException = Mock.Of(x => x.Error == new ArgumentException("Should not be displayed")); + + var context = new DefaultHttpContext(); + context.Features.Set(mockException); + + var controller = new ErrorController(mockEnv, mockLog) + { + ControllerContext = new ControllerContext() { HttpContext = context } + }; + + // Act + IActionResult actionResult = controller.Error(); + + // Assert + var notFoundResult = Assert.IsType(actionResult); + var contentResult = Assert.IsType(notFoundResult.Value); + var expected = "An error occured. Please try again later."; + var actual = contentResult.Error; + Assert.Equal(expected, actual); + } + } +} diff --git a/Server.UnitTest/Shared/ModelValidationTest.cs b/Server.UnitTest/Shared/ModelValidationTest.cs new file mode 100644 index 0000000..8d7a124 --- /dev/null +++ b/Server.UnitTest/Shared/ModelValidationTest.cs @@ -0,0 +1,47 @@ +using System; +using System.Collections.Generic; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Abstractions; +using Microsoft.AspNetCore.Mvc.Filters; +using Microsoft.AspNetCore.Mvc.ModelBinding; +using Microsoft.AspNetCore.Routing; +using Moq; +using Xunit; +using Server.Shared; + +namespace Server.UnitTest.Shared +{ + public class ModelValidationTest + { + [Fact] + public void ModelValidationFilterAttribute_ShouldThrowArgumentException_IfModelIsInvalid() + { + // Arrange + var validationFilter = new ModelValidationFilterAttribute(); + var modelState = new ModelStateDictionary(); + modelState.AddModelError("year", "invalid"); + + var actionContext = new ActionContext( + Mock.Of(), + Mock.Of(), + Mock.Of(), + modelState + ); + var actionExecutingContext = new ActionExecutingContext( + actionContext, + new List(), + new Dictionary(), + Mock.Of() + ); + + // Act + var exception = Record.Exception(() => validationFilter.OnActionExecuting(actionExecutingContext)); + + // Assert + Assert.IsType(exception); + Assert.Equal("invalid", exception.Message); + } + } +} diff --git a/Server/App_Data/claim.json b/Server/App_Data/claim.json new file mode 100644 index 0000000..a0737e6 --- /dev/null +++ b/Server/App_Data/claim.json @@ -0,0 +1,65 @@ +[ + { + "issuer": "LOCAL AUTHORITY", + "originalIssuer": "LOCAL AUTHORITY", + "properties": {}, + "subject": null, + "type": "name", + "value": "Bob Smith", + "valueType": "http://www.w3.org/2001/XMLSchema#string" + }, + { + "issuer": "LOCAL AUTHORITY", + "originalIssuer": "LOCAL AUTHORITY", + "properties": {}, + "subject": null, + "type": "given_name", + "value": "Bob", + "valueType": "http://www.w3.org/2001/XMLSchema#string" + }, + { + "issuer": "LOCAL AUTHORITY", + "originalIssuer": "LOCAL AUTHORITY", + "properties": {}, + "subject": null, + "type": "family_name", + "value": "Smith", + "valueType": "http://www.w3.org/2001/XMLSchema#string" + }, + { + "issuer": "LOCAL AUTHORITY", + "originalIssuer": "LOCAL AUTHORITY", + "properties": {}, + "subject": null, + "type": "email", + "value": "BobSmith@email.com", + "valueType": "http://www.w3.org/2001/XMLSchema#string" + }, + { + "issuer": "LOCAL AUTHORITY", + "originalIssuer": "LOCAL AUTHORITY", + "properties": {}, + "subject": null, + "type": "email_verified", + "value": "true", + "valueType": "http://www.w3.org/2001/XMLSchema#string" + }, + { + "issuer": "LOCAL AUTHORITY", + "originalIssuer": "LOCAL AUTHORITY", + "properties": {}, + "subject": null, + "type": "website", + "value": "http://bob.com", + "valueType": "http://www.w3.org/2001/XMLSchema#string" + }, + { + "issuer": "LOCAL AUTHORITY", + "originalIssuer": "LOCAL AUTHORITY", + "properties": {}, + "subject": null, + "type": "sub", + "value": "11", + "valueType": "http://www.w3.org/2001/XMLSchema#string" + } +] diff --git a/Server/App_Data/create_tables.sql b/Server/App_Data/create_tables.sql new file mode 100644 index 0000000..82a7710 --- /dev/null +++ b/Server/App_Data/create_tables.sql @@ -0,0 +1,26 @@ +DROP TABLE IF EXISTS t_stuff; +DROP TABLE IF EXISTS t_user; +CREATE TABLE t_user +( + usr_id TEXT NOT NULL PRIMARY KEY UNIQUE, + usr_name TEXT, + usr_given_name TEXT, + usr_family_name TEXT, + usr_email TEXT, + usr_created_at TEXT, + usr_updated_at TEXT +); +CREATE TABLE t_stuff +( + stf_id TEXT NOT NULL PRIMARY KEY UNIQUE, + stf_user_id TEXT NOT NULL, + stf_label TEXT NOT NULL, + stf_description TEXT, + stf_other_info TEXT, + stf_created_at TEXT, + stf_updated_at TEXT, + FOREIGN KEY (stf_user_id) + REFERENCES t_user (usr_id) + ON DELETE CASCADE + ON UPDATE NO ACTION +); diff --git a/Server/App_Data/stuff.db b/Server/App_Data/stuff.db new file mode 100644 index 0000000..05e3536 Binary files /dev/null and b/Server/App_Data/stuff.db differ diff --git a/Server/Controllers/StuffController.cs b/Server/Controllers/StuffController.cs new file mode 100644 index 0000000..941cd81 --- /dev/null +++ b/Server/Controllers/StuffController.cs @@ -0,0 +1,60 @@ +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; +using Server.Models; +using Server.Repository.Interfaces; + +namespace Server.Controllers +{ + [Route("api/[controller]")] + public class StuffController : ControllerBase + { + private readonly IStuffRepo _stuffRepo; + + public StuffController(IStuffRepo stuffRepo) + { + _stuffRepo = stuffRepo; + } + + // GET STUFF LIST api/stuff?page=2&search=xxx + [HttpGet] + public async Task GetList([FromQuery] int page, [FromQuery] string search) + { + StuffModel result = string.IsNullOrWhiteSpace(search) + ? await _stuffRepo.GetListAsync(page) + : await _stuffRepo.SearchListAsync(search); + return Ok(result); + } + + // CREATE STUFF api/stuff + {body} + [HttpPost] + public async Task Create([FromBody] DatumModel input) + { + DatumModel result = await _stuffRepo.CreateAsync(input); + return CreatedAtAction(nameof(Read), new { id = result.Id }, result); + } + + // READ STUFF api/stuff/id + [HttpGet("{id}")] + public async Task Read(string id) + { + DatumModel result = await _stuffRepo.ReadAsync(id); + return Ok(result); + } + + // UPDATE STUFF api/stuff/id + {body} + [HttpPut("{id}")] + public async Task Update(string id, [FromBody] DatumModel input) + { + DatumModel result = await _stuffRepo.UpdateAsync(id, input); + return Ok(result); + } + + // DELETE STUFF api/stuff/id + [HttpDelete("{id}")] + public async Task Delete(string id) + { + await _stuffRepo.DeleteAsync(id); + return NoContent(); + } + } +} diff --git a/Server/Controllers/UserController.cs b/Server/Controllers/UserController.cs new file mode 100644 index 0000000..3a827ed --- /dev/null +++ b/Server/Controllers/UserController.cs @@ -0,0 +1,60 @@ +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; +using Server.Models; +using Server.Repository.Interfaces; + +namespace Server.Controllers +{ + [Route("api/[controller]")] + public class UserController : ControllerBase + { + private readonly IUserRepo _userRepo; + + public UserController(IUserRepo userRepo) + { + _userRepo = userRepo; + } + + // GET USER LIST api/user?page=2&search=xxx + [HttpGet] + public async Task GetList([FromQuery] int page, [FromQuery] string search) + { + DirectoryModel result = string.IsNullOrWhiteSpace(search) + ? await _userRepo.GetListAsync(page) + : await _userRepo.SearchListAsync(search); + return Ok(result); + } + + // CREATE USER api/user + {body} + [HttpPost] + public async Task Create([FromBody] UserModel input) + { + UserModel result = await _userRepo.CreateAsync(input); + return CreatedAtAction(nameof(Read), new { id = result.Id }, result); + } + + // READ USER api/user/id + [HttpGet("{id}")] + public async Task Read(string id) + { + UserModel result = await _userRepo.ReadAsync(id); + return Ok(result); + } + + // UPDATE USER api/user/id + {body} + [HttpPut("{id}")] + public async Task Update(string id, [FromBody] UserModel input) + { + UserModel result = await _userRepo.UpdateAsync(id, input); + return Ok(result); + } + + // DELETE USER api/user/id + [HttpDelete("{id}")] + public async Task Delete(string id) + { + await _userRepo.DeleteAsync(id); + return NoContent(); + } + } +} diff --git a/Server/DbModels/StuffDbContext.cs b/Server/DbModels/StuffDbContext.cs new file mode 100644 index 0000000..0516140 --- /dev/null +++ b/Server/DbModels/StuffDbContext.cs @@ -0,0 +1,80 @@ +using Microsoft.EntityFrameworkCore; + +namespace Server.DbModels +{ + public partial class StuffDbContext : DbContext + { + public StuffDbContext() + { + } + + public StuffDbContext(DbContextOptions options) + : base(options) + { + } + + public virtual DbSet TStuff { get; set; } + public virtual DbSet TUser { get; set; } + + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + { + } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder.Entity(entity => + { + entity.HasKey(e => e.StfId); + + entity.ToTable("t_stuff"); + + entity.Property(e => e.StfId).HasColumnName("stf_id"); + + entity.Property(e => e.StfCreatedAt).HasColumnName("stf_created_at"); + + entity.Property(e => e.StfDescription).HasColumnName("stf_description"); + + entity.Property(e => e.StfLabel) + .IsRequired() + .HasColumnName("stf_label"); + + entity.Property(e => e.StfOtherInfo).HasColumnName("stf_other_info"); + + entity.Property(e => e.StfUpdatedAt).HasColumnName("stf_updated_at"); + + entity.Property(e => e.StfUserId) + .IsRequired() + .HasColumnName("stf_user_id"); + + entity.HasOne(d => d.StfUser) + .WithMany(p => p.TStuff) + .HasForeignKey(d => d.StfUserId); + }); + + modelBuilder.Entity(entity => + { + entity.HasKey(e => e.UsrId); + + entity.ToTable("t_user"); + + entity.Property(e => e.UsrId).HasColumnName("usr_id"); + + entity.Property(e => e.UsrCreatedAt).HasColumnName("usr_created_at"); + + entity.Property(e => e.UsrEmail).HasColumnName("usr_email"); + + entity.Property(e => e.UsrFamilyName).HasColumnName("usr_family_name"); + + entity.Property(e => e.UsrGivenName).HasColumnName("usr_given_name"); + + entity.Property(e => e.UsrName).HasColumnName("usr_name"); + + entity.Property(e => e.UsrUpdatedAt).HasColumnName("usr_updated_at"); + }); + + OnModelCreatingPartial(modelBuilder); + } + + partial void OnModelCreatingPartial(ModelBuilder modelBuilder); + } +} diff --git a/Server/DbModels/TStuff.cs b/Server/DbModels/TStuff.cs new file mode 100644 index 0000000..8451fb8 --- /dev/null +++ b/Server/DbModels/TStuff.cs @@ -0,0 +1,18 @@ +using System; +using System.Collections.Generic; + +namespace Server.DbModels +{ + public partial class TStuff + { + public string StfId { get; set; } + public string StfUserId { get; set; } + public string StfLabel { get; set; } + public string StfDescription { get; set; } + public string StfOtherInfo { get; set; } + public string StfCreatedAt { get; set; } + public string StfUpdatedAt { get; set; } + + public virtual TUser StfUser { get; set; } + } +} diff --git a/Server/DbModels/TUser.cs b/Server/DbModels/TUser.cs new file mode 100644 index 0000000..d5f84c2 --- /dev/null +++ b/Server/DbModels/TUser.cs @@ -0,0 +1,23 @@ +using System; +using System.Collections.Generic; + +namespace Server.DbModels +{ + public partial class TUser + { + public TUser() + { + TStuff = new HashSet(); + } + + public string UsrId { get; set; } + public string UsrName { get; set; } + public string UsrGivenName { get; set; } + public string UsrFamilyName { get; set; } + public string UsrEmail { get; set; } + public string UsrCreatedAt { get; set; } + public string UsrUpdatedAt { get; set; } + + public virtual ICollection TStuff { get; set; } + } +} diff --git a/Server/Models/DatumModel.cs b/Server/Models/DatumModel.cs new file mode 100644 index 0000000..4cd5785 --- /dev/null +++ b/Server/Models/DatumModel.cs @@ -0,0 +1,15 @@ +using System; + +namespace Server.Models +{ + public class DatumModel + { + public string Id { get; set; } + public string Label { get; set; } + public string Description { get; set; } + public string OtherInfo { get; set; } + public DateTime? CreatedAt { get; set; } + public DateTime? UpdatedAt { get; set; } + public UserModel User { get; set; } + } +} diff --git a/Server/Models/DirectoryModel.cs b/Server/Models/DirectoryModel.cs new file mode 100644 index 0000000..4fcf451 --- /dev/null +++ b/Server/Models/DirectoryModel.cs @@ -0,0 +1,13 @@ +using System.Collections.Generic; + +namespace Server.Models +{ + public class DirectoryModel + { + public int Page { get; set; } + public int PerPage { get; set; } + public long Total { get; set; } + public int TotalPages { get; set; } + public ICollection UserList { get; set; } + } +} diff --git a/Server/Models/StuffModel.cs b/Server/Models/StuffModel.cs new file mode 100644 index 0000000..d4e4fe3 --- /dev/null +++ b/Server/Models/StuffModel.cs @@ -0,0 +1,13 @@ +using System.Collections.Generic; + +namespace Server.Models +{ + public class StuffModel + { + public int Page { get; set; } + public int PerPage { get; set; } + public long Total { get; set; } + public int TotalPages { get; set; } + public ICollection DatumList { get; set; } + } +} diff --git a/Server/Models/UserModel.cs b/Server/Models/UserModel.cs new file mode 100644 index 0000000..7327d06 --- /dev/null +++ b/Server/Models/UserModel.cs @@ -0,0 +1,15 @@ +using System; + +namespace Server.Models +{ + public class UserModel + { + public string Id { get; set; } + public string Name { get; set; } + public string GivenName { get; set; } + public string FamilyName { get; set; } + public string Email { get; set; } + public DateTime? CreatedAt { get; set; } + public DateTime? UpdatedAt { get; set; } + } +} diff --git a/Server/Program.cs b/Server/Program.cs new file mode 100644 index 0000000..0def049 --- /dev/null +++ b/Server/Program.cs @@ -0,0 +1,20 @@ +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Hosting; + +namespace Server +{ + public class Program + { + public static void Main(string[] args) + { + CreateHostBuilder(args).Build().Run(); + } + + public static IHostBuilder CreateHostBuilder(string[] args) => + Host.CreateDefaultBuilder(args) + .ConfigureWebHostDefaults(webBuilder => + { + webBuilder.UseStartup(); + }); + } +} diff --git a/Server/Repository/Checker.cs b/Server/Repository/Checker.cs new file mode 100644 index 0000000..e57a619 --- /dev/null +++ b/Server/Repository/Checker.cs @@ -0,0 +1,39 @@ +using System; +using Server.Models; + +namespace Server.Repository +{ + public static class Checker + { + public static void CheckUser(this UserModel input) + { + if (string.IsNullOrWhiteSpace(input.Id)) + { + throw new ArgumentException("The id cannot be empty."); + } + + if (string.IsNullOrWhiteSpace(input.GivenName)) + { + throw new ArgumentException("The given name cannot be empty."); + } + + if (string.IsNullOrWhiteSpace(input.FamilyName)) + { + throw new ArgumentException("The family name cannot be empty."); + } + + if (string.IsNullOrWhiteSpace(input.Email)) + { + throw new ArgumentException("The email cannot be empty."); + } + } + + public static void CheckDatum(this DatumModel input) + { + if (string.IsNullOrWhiteSpace(input.Label)) + { + throw new ArgumentException("The label cannot be empty."); + } + } + } +} diff --git a/Server/Repository/DateTranslator.cs b/Server/Repository/DateTranslator.cs new file mode 100644 index 0000000..bcde412 --- /dev/null +++ b/Server/Repository/DateTranslator.cs @@ -0,0 +1,19 @@ +using System; + +namespace Server.Repository +{ + public static class DateTranslator + { + public static DateTime? ToUtcDate(this string input) + { + var result = string.IsNullOrEmpty(input) ? (DateTime?)null : DateTime.Parse(input).ToUniversalTime(); + return result; + } + + public static string ToStrDate(this DateTime input) + { + var result = input.ToString("o"); + return result; + } + } +} diff --git a/Server/Repository/DirectoryTranslator.cs b/Server/Repository/DirectoryTranslator.cs new file mode 100644 index 0000000..b937afc --- /dev/null +++ b/Server/Repository/DirectoryTranslator.cs @@ -0,0 +1,73 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Server.DbModels; +using Server.Models; + +namespace Server.Repository +{ + public static class DirectoryTranslator + { + public static UserModel ToUserModel(this TUser input) + { + var result = input == null ? null : new UserModel + { + Id = input.UsrId, + Name = input.UsrName, + GivenName = input.UsrGivenName, + FamilyName = input.UsrFamilyName, + Email = input.UsrEmail, + CreatedAt = input.UsrCreatedAt.ToUtcDate(), + UpdatedAt = input.UsrUpdatedAt.ToUtcDate() + }; + + return result; + } + + public static ICollection ToUserListModel(this ICollection input) + { + var result = input?.Select(x => x.ToUserModel()).ToList(); + return result; + } + + public static DirectoryModel ToDirectoryModel(this ICollection input, int page, int dbCount, int totalPages, int itemsPerPage) + { + var result = new DirectoryModel + { + Page = page, + PerPage = itemsPerPage, + Total = dbCount, + TotalPages = totalPages, + UserList = input.ToUserListModel() + }; + + return result; + } + + public static TUser ToCreate(this UserModel input) + { + var result = input == null ? null : new TUser + { + UsrId = input.Id, + UsrName = $"{input.GivenName} {input.FamilyName}", + UsrGivenName = input.GivenName, + UsrFamilyName = input.FamilyName, + UsrEmail = input.Email, + UsrCreatedAt = DateTime.UtcNow.ToStrDate() + }; + + return result; + } + + public static TUser ToUpdate(this UserModel input, TUser result) + { + result.UsrId = input.Id; + result.UsrName = $"{input.GivenName} {input.FamilyName}"; + result.UsrGivenName = input.GivenName; + result.UsrFamilyName = input.FamilyName; + result.UsrEmail = input.Email; + result.UsrUpdatedAt = DateTime.UtcNow.ToStrDate(); + return result; + } + } +} diff --git a/Server/Repository/Interfaces/IStuffRepo.cs b/Server/Repository/Interfaces/IStuffRepo.cs new file mode 100644 index 0000000..8144031 --- /dev/null +++ b/Server/Repository/Interfaces/IStuffRepo.cs @@ -0,0 +1,15 @@ +using System.Threading.Tasks; +using Server.Models; + +namespace Server.Repository.Interfaces +{ + public interface IStuffRepo + { + Task GetListAsync(int page); + Task SearchListAsync(string search); + Task CreateAsync(DatumModel input); + Task ReadAsync(string stuffId); + Task UpdateAsync(string stuffId, DatumModel input); + Task DeleteAsync(string stuffId); + } +} diff --git a/Server/Repository/Interfaces/IUserAuthRepo.cs b/Server/Repository/Interfaces/IUserAuthRepo.cs new file mode 100644 index 0000000..460a403 --- /dev/null +++ b/Server/Repository/Interfaces/IUserAuthRepo.cs @@ -0,0 +1,10 @@ +using System.Threading.Tasks; +using Server.DbModels; + +namespace Server.Repository.Interfaces +{ + public interface IUserAuthRepo + { + Task GetCurrentUserAsync(string operation); + } +} diff --git a/Server/Repository/Interfaces/IUserRepo.cs b/Server/Repository/Interfaces/IUserRepo.cs new file mode 100644 index 0000000..d7b713f --- /dev/null +++ b/Server/Repository/Interfaces/IUserRepo.cs @@ -0,0 +1,15 @@ +using System.Threading.Tasks; +using Server.Models; + +namespace Server.Repository.Interfaces +{ + public interface IUserRepo + { + Task GetListAsync(int page); + Task SearchListAsync(string search); + Task CreateAsync(UserModel input); + Task ReadAsync(string userId); + Task UpdateAsync(string userId, UserModel input); + Task DeleteAsync(string userId); + } +} diff --git a/Server/Repository/StuffRepo.cs b/Server/Repository/StuffRepo.cs new file mode 100644 index 0000000..0af0b99 --- /dev/null +++ b/Server/Repository/StuffRepo.cs @@ -0,0 +1,149 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; +using Server.DbModels; +using Server.Models; +using Server.Repository.Interfaces; +using Server.Shared; + +namespace Server.Repository +{ + public class StuffRepo : IStuffRepo + { + private const int CstItemsPerPage = 6; + private readonly IUserAuthRepo _userAuth; + private readonly StuffDbContext _context; + + public StuffRepo(StuffDbContext context, IUserAuthRepo userAuth) + { + _context = context; + _userAuth = userAuth; + } + + public async Task GetListAsync(int page) + { + if (page == 0) + { + page = 1; + } + + int dbCount = await _context.TStuff.CountAsync(); + int totalPages = ((dbCount - 1) / CstItemsPerPage) + 1; + if (dbCount == 0 || page > totalPages) + { + page = 1; + } + + ICollection dbStuffList = await _context.TStuff.AsQueryable() + .OrderByDescending(x => x.StfUpdatedAt) + .ThenByDescending(x => x.StfCreatedAt) + .Skip(CstItemsPerPage * (page - 1)) + .Take(CstItemsPerPage) + .Include(x => x.StfUser) + .ToListAsync(); + var result = dbStuffList.ToStuffModel(page, dbCount, totalPages, CstItemsPerPage); + return result; + } + + public async Task SearchListAsync(string search) + { + IQueryable query = _context.TStuff.Include(x => x.StfUser).Where(x => + EF.Functions.Like(x.StfLabel, $"%{search}%") + || EF.Functions.Like(x.StfDescription, $"%{search}%") + || EF.Functions.Like(x.StfOtherInfo, $"%{search}%") + || EF.Functions.Like(x.StfUser.UsrGivenName, $"%{search}%") + || EF.Functions.Like(x.StfUser.UsrFamilyName, $"%{search}%") + ); + + int dbCount = await query.CountAsync(); + if (dbCount > CstItemsPerPage) + { + throw new ArgumentException("Too many results. Please narrow your search."); + } + + // Get stuff and their users. + ICollection dbStuffList = await query.Include(x => x.StfUser).ToListAsync(); + var result = dbStuffList.ToStuffModel(1, dbCount, 1, CstItemsPerPage); + return result; + } + + public async Task CreateAsync(DatumModel input) + { + input.CheckDatum(); + TStuff dbStuff = input.ToCreate(); + TUser dbUserAuth = await _userAuth.GetCurrentUserAsync("created datum"); + TUser dbUser = await _context.TUser.FirstOrDefaultAsync(x => x.UsrId.Equals(dbUserAuth.UsrId)); + if (dbUser == null) + { // Create and attach new user + dbUserAuth.UsrCreatedAt = DateTime.UtcNow.ToStrDate(); + dbStuff.StfUser = dbUserAuth; + } + + // Attach foreign key + dbStuff.StfUserId = dbUserAuth.UsrId; + // Create stuff + await _context.TStuff.AddAsync(dbStuff); + await _context.SaveChangesAsync(); + // Attach user to the stuff for the response + dbStuff.StfUser = dbUserAuth; + var result = dbStuff.ToDatumModel(); + return result; + } + + public async Task ReadAsync(string stuffId) + { + // Get the stuff and its user + TStuff dbStuff = await _context.TStuff + .Where(x => x.StfId.Equals(stuffId)) + .Include(x => x.StfUser) + .FirstOrDefaultAsync(); + + if (dbStuff == null) + { + throw new NotFoundException("Stuff not found."); + } + + var result = dbStuff.ToDatumModel(); + return result; + } + + public async Task UpdateAsync(string stuffId, DatumModel input) + { + input.CheckDatum(); + if (stuffId != input.Id) + { + throw new ArgumentException("Corrupted data."); + } + + TUser dbUserAuth = await _userAuth.GetCurrentUserAsync("updated datum"); + TStuff dbStuff = await _context.TStuff.FirstOrDefaultAsync(x => x.StfId.Equals(stuffId)); + if (dbStuff == null || dbStuff.StfUserId != dbUserAuth.UsrId) + { + throw new ArgumentException("Corrupted data."); + } + + // Update stuff + dbStuff = input.ToUpdate(dbStuff); + await _context.SaveChangesAsync(); + // Attach user to the stuff for the response + dbStuff.StfUser = dbUserAuth; + var result = dbStuff.ToDatumModel(); + return result; + } + + public async Task DeleteAsync(string stuffId) + { + TUser dbUserAuth = await _userAuth.GetCurrentUserAsync("deleted datum"); + TStuff dbStuff = await _context.TStuff.FirstOrDefaultAsync(x => x.StfId.Equals(stuffId)); + if (dbStuff == null || dbStuff.StfUserId != dbUserAuth.UsrId) + { + throw new ArgumentException("Corrupted data."); + } + + _context.TStuff.Remove(dbStuff); + await _context.SaveChangesAsync(); + } + } +} diff --git a/Server/Repository/StuffTranslator.cs b/Server/Repository/StuffTranslator.cs new file mode 100644 index 0000000..ddb8736 --- /dev/null +++ b/Server/Repository/StuffTranslator.cs @@ -0,0 +1,74 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Server.DbModels; +using Server.Models; + +namespace Server.Repository +{ + public static class StuffTranslator + { + public static DatumModel ToDatumModel(this TStuff input) + { + var result = input == null ? null : new DatumModel + { + Id = input.StfId, + Label = input.StfLabel, + Description = input.StfDescription, + OtherInfo = input.StfOtherInfo, + CreatedAt = input.StfCreatedAt.ToUtcDate(), + UpdatedAt = input.StfUpdatedAt.ToUtcDate(), + User = input.StfUser.ToUserModel() + }; + + return result; + } + + public static ICollection ToDatumListModel(this ICollection input) + { + var result = input?.Select(x => x.ToDatumModel()).ToList(); + return result; + } + + public static StuffModel ToStuffModel(this ICollection input, int page, int dbCount, int totalPages, int itemsPerPage) + { + var result = new StuffModel + { + Page = page, + PerPage = itemsPerPage, + Total = dbCount, + TotalPages = totalPages, + DatumList = input.ToDatumListModel() + }; + + return result; + } + + public static TStuff ToCreate(this DatumModel input) + { + string now = DateTime.UtcNow.ToStrDate(); + var result = input == null ? null : new TStuff + { + StfId = Guid.NewGuid().ToString(), + StfLabel = input.Label, + StfDescription = input.Description, + StfOtherInfo = input.OtherInfo, + StfCreatedAt = now, + StfUpdatedAt = now + }; + + return result; + } + + public static TStuff ToUpdate(this DatumModel input, TStuff result) + { + result.StfId = input.Id; + result.StfLabel = input.Label; + result.StfDescription = input.Description; + result.StfOtherInfo = input.OtherInfo; + result.StfUpdatedAt = DateTime.UtcNow.ToStrDate(); + + return result; + } + } +} diff --git a/Server/Repository/UserAuthRepo.cs b/Server/Repository/UserAuthRepo.cs new file mode 100644 index 0000000..e2e0c5a --- /dev/null +++ b/Server/Repository/UserAuthRepo.cs @@ -0,0 +1,84 @@ +using System; +using System.Collections.Generic; +using System.IdentityModel.Tokens.Jwt; +using System.Linq; +using System.Net.Http; +using System.Security.Claims; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Caching.Memory; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; +using IdentityModel.Client; +using Server.DbModels; +using Server.Repository.Interfaces; + +namespace Server.Repository +{ + public class UserAuthRepo : IUserAuthRepo + { + private const string CstCachePrefix = "AUTH_"; + private readonly IConfiguration _conf; + private readonly IMemoryCache _cache; + private readonly IHttpContextAccessor _httpContext; + private readonly ILogger _logger; + + public UserAuthRepo(IMemoryCache cache, IConfiguration conf, IHttpContextAccessor httpContext, ILogger logger) + { + _cache = cache; + _conf = conf; + _httpContext = httpContext; + _logger = logger; + } + + public async Task GetCurrentUserAsync(string operation) + { + var accessToken = await _httpContext.HttpContext.GetTokenAsync("access_token"); + + string cacheKey = $"{CstCachePrefix}{accessToken}"; + if (_cache.TryGetValue(cacheKey, out TUser result)) + { + _logger.LogInformation($"{result.UsrName} has {operation} at {DateTime.Now}"); + return result; + } + + IEnumerable userClaimList; + using (var httpClient = new HttpClient()) + { + var userInfo = await httpClient.GetUserInfoAsync(new UserInfoRequest + { + Address = _conf["JwtToken:UserInfoService"], + Token = accessToken + }); + + if (userInfo.IsError) + { + throw new ArgumentException(userInfo.Error); + } + + userClaimList = userInfo.Claims; + } + + JwtSecurityToken decodeSub = new JwtSecurityTokenHandler().ReadJwtToken(accessToken); + var claimList = userClaimList.ToList(); + result = new TUser + { + UsrId = decodeSub?.Claims.FirstOrDefault(x => x.Type == JwtRegisteredClaimNames.Sub)?.Value, + UsrName = claimList.FirstOrDefault(x => x.Type == "name")?.Value, + UsrGivenName = claimList.FirstOrDefault(x => x.Type == JwtRegisteredClaimNames.GivenName)?.Value, + UsrFamilyName = claimList.FirstOrDefault(x => x.Type == JwtRegisteredClaimNames.FamilyName)?.Value, + UsrEmail = claimList.FirstOrDefault(x => x.Type == JwtRegisteredClaimNames.Email)?.Value + }; + + var cacheEntryOptions = new MemoryCacheEntryOptions + { + AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(50), + }; + _cache.Set(cacheKey, result, cacheEntryOptions); + + _logger.LogInformation($"{result.UsrName} has {operation} at {DateTime.Now}"); + return result; + } + } +} diff --git a/Server/Repository/UserRepo.cs b/Server/Repository/UserRepo.cs new file mode 100644 index 0000000..eee9183 --- /dev/null +++ b/Server/Repository/UserRepo.cs @@ -0,0 +1,119 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; +using Server.DbModels; +using Server.Models; +using Server.Repository.Interfaces; + +namespace Server.Repository +{ + public class UserRepo : IUserRepo + { + private const int CstItemsPerPage = 6; + private readonly IUserAuthRepo _userAuth; + private readonly StuffDbContext _context; + + public UserRepo(StuffDbContext context, IUserAuthRepo userAuth) + { + _context = context; + _userAuth = userAuth; + } + + public async Task GetListAsync(int page) + { + if (page == 0) + { + page = 1; + } + + int dbCount = await _context.TUser.CountAsync(); + int totalPages = ((dbCount - 1) / CstItemsPerPage) + 1; + if (dbCount == 0 || page > totalPages) + { + page = 1; + totalPages = 1; + } + + ICollection dbUserList = await _context.TUser + .OrderByDescending(x => x.UsrUpdatedAt) + .ThenByDescending(x => x.UsrCreatedAt) + .Skip(CstItemsPerPage * (page - 1)) + .Take(CstItemsPerPage) + .ToListAsync(); + var result = dbUserList.ToDirectoryModel(page, dbCount, totalPages, CstItemsPerPage); + return result; + } + + public async Task SearchListAsync(string search) + { + IQueryable query = _context.TUser.Where(x => + EF.Functions.Like(x.UsrGivenName, $"%{search}%") + || EF.Functions.Like(x.UsrFamilyName, $"%{search}%") + ); + + int dbCount = await query.CountAsync(); + if (dbCount > CstItemsPerPage) + { + throw new ArgumentException("Too many results. Please narrow your search."); + } + + List dbUserList = await query.ToListAsync(); + var result = dbUserList.ToDirectoryModel(1, dbCount, 1, CstItemsPerPage); + return result; + } + + public async Task CreateAsync(UserModel input) + { + input.CheckUser(); + TUser dbUser = input.ToCreate(); + await _context.TUser.AddAsync(dbUser); + await _context.SaveChangesAsync(); + var result = dbUser.ToUserModel(); + return result; + } + + public async Task ReadAsync(string userId) + { + TUser dbUser = await _context.TUser.FirstOrDefaultAsync(x => x.UsrId.Equals(userId)); + var result = dbUser.ToUserModel(); + return result; + } + + public async Task UpdateAsync(string userId, UserModel input) + { + input.CheckUser(); + + if (userId != input.Id) + { + throw new ArgumentException("Corrupted data."); + } + + TUser dbUserAuth = await _userAuth.GetCurrentUserAsync("updated user"); + TUser dbUser = await _context.TUser.FirstOrDefaultAsync(x => x.UsrId.Equals(userId)); + if (dbUser == null || dbUser.UsrId != dbUserAuth.UsrId) + { + throw new ArgumentException("Corrupted data."); + } + + dbUser = input.ToUpdate(dbUser); + await _context.SaveChangesAsync(); + var result = dbUser.ToUserModel(); + return result; + } + + public async Task DeleteAsync(string userId) + { + TUser dbUserAuth = await _userAuth.GetCurrentUserAsync("deleted user"); + TUser dbUser = await _context.TUser.FirstOrDefaultAsync(x => x.UsrId.Equals(userId)); + if (dbUser == null || dbUser.UsrId != dbUserAuth.UsrId) + { + throw new ArgumentException("Corrupted data."); + } + + _context.TUser.Remove(dbUser); + await _context.SaveChangesAsync(); + } + } +} diff --git a/Server/Server.csproj b/Server/Server.csproj new file mode 100644 index 0000000..d5db318 --- /dev/null +++ b/Server/Server.csproj @@ -0,0 +1,19 @@ + + + + net5.0 + + + + + Always + + + + + + + + + + diff --git a/Server/Shared/ErrorController.cs b/Server/Shared/ErrorController.cs new file mode 100644 index 0000000..2544515 --- /dev/null +++ b/Server/Shared/ErrorController.cs @@ -0,0 +1,45 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Diagnostics; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; + +namespace Server.Shared +{ + [Route("api/[controller]")] + [ApiExplorerSettings(IgnoreApi = true)] + public class ErrorController : ControllerBase + { + private readonly IWebHostEnvironment _env; + private readonly ILogger _logger; + + public ErrorController(IWebHostEnvironment env, ILogger logger) + { + _env = env; + _logger = logger; + } + + public IActionResult Error() + { + var context = HttpContext.Features.Get(); + if (context == null) + { + return Ok(); + } + + var exception = context.Error; + _logger.LogCritical(exception, "Error"); + var msg = exception.InnerException == null + ? exception.Message + : exception.InnerException.Message; + + if (exception is NotFoundException) + { + return new NotFoundObjectResult(new ErrorModel { Error = msg }); + } + + var error = new ErrorModel { Error = _env.IsDevelopment() ? msg : "An error occured. Please try again later." }; + return new BadRequestObjectResult(error); + } + } +} diff --git a/Server/Shared/ErrorModel.cs b/Server/Shared/ErrorModel.cs new file mode 100644 index 0000000..2d404fc --- /dev/null +++ b/Server/Shared/ErrorModel.cs @@ -0,0 +1,7 @@ +namespace Server.Shared +{ + public class ErrorModel + { + public string Error { get; set; } + } +} diff --git a/Server/Shared/ModelValidationFilterAttribute.cs b/Server/Shared/ModelValidationFilterAttribute.cs new file mode 100644 index 0000000..7499527 --- /dev/null +++ b/Server/Shared/ModelValidationFilterAttribute.cs @@ -0,0 +1,32 @@ +using System; +using System.Linq; +using Microsoft.AspNetCore.Mvc.Filters; + +namespace Server.Shared +{ + [AttributeUsage(AttributeTargets.Class, AllowMultiple = true)] + public class ModelValidationFilterAttribute : ActionFilterAttribute + { + public override void OnActionExecuting(ActionExecutingContext context) + { + if (context == null) + { + base.OnActionExecuting(null); + return; + } + + if (!context.ModelState.IsValid) + { + var err = context.ModelState.Values.SelectMany(value => value.Errors).FirstOrDefault(); + if (err == null) + { + throw new ArgumentException("Model state is invalid."); + } + + throw new ArgumentException(err.ErrorMessage); + } + + base.OnActionExecuting(context); + } + } +} diff --git a/Server/Shared/NotFoundException.cs b/Server/Shared/NotFoundException.cs new file mode 100644 index 0000000..5214f24 --- /dev/null +++ b/Server/Shared/NotFoundException.cs @@ -0,0 +1,12 @@ +using System; + +namespace Server.Shared +{ + public class NotFoundException : Exception + { + public NotFoundException(string message) + : base(message) + { + } + } +} diff --git a/Server/Shared/TraceHandlerFilterAttribute.cs b/Server/Shared/TraceHandlerFilterAttribute.cs new file mode 100644 index 0000000..17cf771 --- /dev/null +++ b/Server/Shared/TraceHandlerFilterAttribute.cs @@ -0,0 +1,71 @@ +using System.Text.Json; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Filters; +using Microsoft.Extensions.Logging; + +namespace Server.Shared +{ + [System.AttributeUsage(System.AttributeTargets.Class, AllowMultiple = true)] + public sealed class TraceHandlerFilterAttribute : ActionFilterAttribute + { + private readonly ILogger _logger; + + public TraceHandlerFilterAttribute(ILogger logger) + { + _logger = logger; + } + + public override void OnActionExecuting(ActionExecutingContext context) + { + if (context == null) + { + base.OnActionExecuting(null); + return; + } + + string operation = context.HttpContext.Request.Path.ToString(); + foreach (var elt in context.ActionArguments) + { + var flux = JsonSerializer.Serialize(elt.Value, new JsonSerializerOptions { WriteIndented = true }); + _logger.LogInformation($"{operation}_Request : {flux}"); + } + + base.OnActionExecuting(context); + } + + public override void OnActionExecuted(ActionExecutedContext context) + { + if (context == null) + { + base.OnActionExecuted(null); + return; + } + + string operation = context.HttpContext.Request.Path.ToString(); + var result = context.Result; + if (result is ObjectResult elt) + { + string flux = JsonSerializer.Serialize(elt.Value, new JsonSerializerOptions { WriteIndented = true }).Truncate(); + _logger.LogInformation($"{operation}_Response : {flux}"); + } + + base.OnActionExecuted(context); + } + } + + // Truncate trace + public static class StringExt + { + private const int CstMaxLength = 2048; + + public static string Truncate(this string value) + { + if (string.IsNullOrEmpty(value)) + { + return value; + } + + return value.Length <= CstMaxLength ? value : value.Substring(0, CstMaxLength); + } + } +} diff --git a/Server/Startup.cs b/Server/Startup.cs new file mode 100644 index 0000000..4ee739a --- /dev/null +++ b/Server/Startup.cs @@ -0,0 +1,175 @@ +using System; +using System.Collections.Generic; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.ApplicationInsights; +using Microsoft.OpenApi.Models; +using Server.DbModels; +using Server.Repository; +using Server.Repository.Interfaces; +using Server.Shared; + +namespace Server +{ + public class Startup + { + private readonly IConfiguration _conf; + private readonly IWebHostEnvironment _env; + + public Startup(IConfiguration conf, IWebHostEnvironment env) + { + _conf = conf; + _env = env; + } + + // This method gets called by the runtime. Use this method to add services to the container. + public void ConfigureServices(IServiceCollection services) + { + services.AddControllers(); + services.AddCors(); + services.AddMemoryCache(); + services.AddHttpContextAccessor(); + services.AddLogging(builder => + { + builder.AddApplicationInsights(_conf["APPINSIGHTS_INSTRUMENTATIONKEY"]); + builder.AddFilter("", LogLevel.Trace); + builder.AddFilter("Microsoft", LogLevel.Error); + }); + services.AddApplicationInsightsTelemetry(); + + // Add DB + services.AddDbContext(options => options.UseSqlite( + _conf.GetConnectionString("SqlConnectionString"), + sqlServerOptions => sqlServerOptions.CommandTimeout(_conf.GetSection("ConnectionStrings:SqlCommandTimeout").Get())) + ); + + // Add Authent + services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) + .AddJwtBearer(options => + { + options.Authority = _conf["JwtToken:Authority"]; + options.Audience = _conf["JwtToken:Audience"]; + }); + + services.AddMvc(options => + { + options.Filters.Add(typeof(TraceHandlerFilterAttribute)); + options.Filters.Add(typeof(ModelValidationFilterAttribute)); + }).SetCompatibilityVersion(CompatibilityVersion.Latest); + + services.Configure(options => + { // Managed by Shared/ModelValidationFilter.cs + options.SuppressModelStateInvalidFilter = true; + }); + + var now = DateTime.Now; + services.AddHsts(configureOptions => + { + configureOptions.Preload = true; + configureOptions.IncludeSubDomains = true; + configureOptions.MaxAge = now.AddYears(1) - now; + }); + + services.AddHttpsRedirection(options => + { + options.RedirectStatusCode = StatusCodes.Status308PermanentRedirect; + options.HttpsPort = _conf.GetSection("https_port").Get(); + }); + + // Register the Swagger generator + services.AddSwaggerGen(c => + { + c.SwaggerDoc("v1", new OpenApiInfo { Title = "Server", Version = "v1" }); + c.AddSecurityDefinition(JwtBearerDefaults.AuthenticationScheme, new OpenApiSecurityScheme + { + Name = "Authorization", + Type = SecuritySchemeType.ApiKey, + Scheme = JwtBearerDefaults.AuthenticationScheme, + BearerFormat = "JWT", + In = ParameterLocation.Header, + Description = "JWT Authorization header using the Bearer scheme." + }); + c.AddSecurityRequirement(new OpenApiSecurityRequirement + { + { + new OpenApiSecurityScheme + { + Reference = new OpenApiReference + { + Type = ReferenceType.SecurityScheme, + Id = JwtBearerDefaults.AuthenticationScheme + } + }, + new string[] {} + } + }); + }); + + // Register Repo + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + } + + // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. + public void Configure(IApplicationBuilder app) + { + if (_env.IsDevelopment()) + { + app.UseCors(options => options.AllowAnyOrigin().AllowAnyMethod().AllowAnyHeader()); + } + else + { + var corsList = _conf.GetSection("AuthCors").Get(); + app.UseCors(builder => builder + .WithOrigins(corsList) + .AllowAnyMethod() + .AllowAnyHeader() + ); + } + + app.UseFileServer(new FileServerOptions + { + EnableDirectoryBrowsing = false, + EnableDefaultFiles = true, + DefaultFilesOptions = { DefaultFileNames = { "index.html" } } + }); + + if (!_env.IsProduction()) + { + app.UseSwagger(c => + { + c.PreSerializeFilters.Add((swagger, httpReq) => + { + swagger.Servers = new List { new OpenApiServer { Url = $"{httpReq.Scheme}://{httpReq.Host.Value}" } }; + }); + }); + app.UseSwaggerUI(c => + { + c.SwaggerEndpoint("/swagger/v1/swagger.json", "Server V1"); + }); + } + + app.UseExceptionHandler("/api/Error"); + app.UseHsts(); + app.UseHttpsRedirection(); + app.UseStaticFiles(); + app.UseRouting(); + app.UseAuthentication(); + app.UseAuthorization(); + app.UseEndpoints(endpoints => + { + endpoints.MapControllers().RequireAuthorization(); + endpoints.MapFallbackToFile("/index.html"); + }); + } + } +} diff --git a/Server/appsettings.Development.json b/Server/appsettings.Development.json new file mode 100644 index 0000000..69e9774 --- /dev/null +++ b/Server/appsettings.Development.json @@ -0,0 +1,7 @@ +{ + "ASPNETCORE_ENVIRONMENT": "Development", + "AuthCors": [ + "http://localhost:3000", + "https://localhost:5001" + ] +} \ No newline at end of file diff --git a/Server/appsettings.Production.json b/Server/appsettings.Production.json new file mode 100644 index 0000000..af22a67 --- /dev/null +++ b/Server/appsettings.Production.json @@ -0,0 +1,6 @@ +{ + "ASPNETCORE_ENVIRONMENT": "Production", + "AuthCors": [ + "" + ] +} \ No newline at end of file diff --git a/Server/appsettings.Staging.json b/Server/appsettings.Staging.json new file mode 100644 index 0000000..e8ae29b --- /dev/null +++ b/Server/appsettings.Staging.json @@ -0,0 +1,7 @@ +{ + "ASPNETCORE_ENVIRONMENT": "Staging", + "AuthCors": [ + "http://localhost:3000", + "https://localhost:5001" + ] +} \ No newline at end of file diff --git a/Server/appsettings.json b/Server/appsettings.json new file mode 100644 index 0000000..5c26b89 --- /dev/null +++ b/Server/appsettings.json @@ -0,0 +1,21 @@ +{ + "https_port": 443, + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + }, + "AllowedHosts": "*", + "ConnectionStrings": { + "SqlConnectionString": "Data Source=App_Data/stuff.db", + "SqlCommandTimeout": "5" + }, + "JwtToken": { + "Authority": "https://demo.identityserver.io", + "Audience": "api", + "UserInfoService": "https://demo.identityserver.io/connect/userinfo" + }, + "APPINSIGHTS_INSTRUMENTATIONKEY": "02de38e7-0430-425f-ae7e-977536521530" +} diff --git a/Server/web.config b/Server/web.config new file mode 100644 index 0000000..5e45ff0 --- /dev/null +++ b/Server/web.config @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/buildall.sh b/buildall.sh new file mode 100644 index 0000000..fde6196 --- /dev/null +++ b/buildall.sh @@ -0,0 +1,31 @@ +#!/bin/bash +step=0 +STEPS=5 + +clear + +let "step++" +printf "\n************************** $step/$STEPS : Building client...\n" +cd client +npm install +rm -rfv public/build +npm run build + +let "step++" +printf "\n************************** $step/$STEPS : Copying client build to wwwroot server folder...\n" +rm -rfv ../Server/wwwroot/* +printf "\n" +cp -rfv public/. ../Server/wwwroot + +let "step++" +printf "\n************************** $step/$STEPS : Testing server...\n" +dotnet test ../Server.UnitTest + +let "step++" +printf "\n************************** $step/$STEPS : Building server...\n" +dotnet build ../Server --configuration Release + +cd .. +printf "\nDone.\n\n" +# cd Server +# dotnet bin/Release/net5.0/Server.dll diff --git a/client/package-lock.json b/client/package-lock.json new file mode 100644 index 0000000..c86531c --- /dev/null +++ b/client/package-lock.json @@ -0,0 +1,2768 @@ +{ + "name": "svelte-netcore-identity", + "version": "1.0.0", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "name": "svelte-netcore-identity", + "version": "1.0.0", + "dependencies": { + "oidc-client": "^1.11.5", + "sirv-cli": "^1.0.14" + }, + "devDependencies": { + "@rollup/plugin-commonjs": "^17.0.0", + "@rollup/plugin-node-resolve": "^11.0.0", + "@rollup/plugin-replace": "^3.0.0", + "axios": "^0.21.4", + "bootstrap": "^5.1.3", + "dotenv": "^10.0.0", + "rollup": "^2.3.4", + "rollup-plugin-css-only": "^3.1.0", + "rollup-plugin-json": "^4.0.0", + "rollup-plugin-livereload": "^2.0.0", + "rollup-plugin-svelte": "^7.0.0", + "rollup-plugin-terser": "^7.0.0", + "svelte": "^3.0.0", + "svelte-navigator": "^3.1.5", + "svelte-preprocess": "^4.9.8" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.15.8", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.15.8.tgz", + "integrity": "sha512-2IAnmn8zbvC/jKYhq5Ki9I+DwjlrtMPUCH/CpHvqI4dNnlwHwsxoIhlc8WcYY5LSYknXQtAlFYuHfqAFCvQ4Wg==", + "dev": true, + "dependencies": { + "@babel/highlight": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.15.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.15.7.tgz", + "integrity": "sha512-K4JvCtQqad9OY2+yTU8w+E82ywk/fe+ELNlt1G8z3bVGlZfn/hOcQQsUhGhW/N+tb3fxK800wLtKOE/aM0m72w==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/highlight": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.14.5.tgz", + "integrity": "sha512-qf9u2WFWVV0MppaL877j2dBtQIDgmidgjGk5VIMw3OadXvYaXn66U1BFlH2t4+t3i+8PhedppRv+i40ABzd+gg==", + "dev": true, + "dependencies": { + "@babel/helper-validator-identifier": "^7.14.5", + "chalk": "^2.0.0", + "js-tokens": "^4.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@polka/url": { + "version": "1.0.0-next.21", + "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.21.tgz", + "integrity": "sha512-a5Sab1C4/icpTZVzZc5Ghpz88yQtGOyNqYXcZgOssB2uuAr+wF/MvN6bgtW32q7HHrvBki+BsZ0OuNv6EV3K9g==" + }, + "node_modules/@popperjs/core": { + "version": "2.10.2", + "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.10.2.tgz", + "integrity": "sha512-IXf3XA7+XyN7CP9gGh/XB0UxVMlvARGEgGXLubFICsUMGz6Q+DU+i4gGlpOxTjKvXjkJDJC8YdqdKkDj9qZHEQ==", + "dev": true, + "peer": true, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/popperjs" + } + }, + "node_modules/@rollup/plugin-commonjs": { + "version": "17.1.0", + "resolved": "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-17.1.0.tgz", + "integrity": "sha512-PoMdXCw0ZyvjpCMT5aV4nkL0QywxP29sODQsSGeDpr/oI49Qq9tRtAsb/LbYbDzFlOydVEqHmmZWFtXJEAX9ew==", + "dev": true, + "dependencies": { + "@rollup/pluginutils": "^3.1.0", + "commondir": "^1.0.1", + "estree-walker": "^2.0.1", + "glob": "^7.1.6", + "is-reference": "^1.2.1", + "magic-string": "^0.25.7", + "resolve": "^1.17.0" + }, + "engines": { + "node": ">= 8.0.0" + }, + "peerDependencies": { + "rollup": "^2.30.0" + } + }, + "node_modules/@rollup/plugin-node-resolve": { + "version": "11.2.1", + "resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-11.2.1.tgz", + "integrity": "sha512-yc2n43jcqVyGE2sqV5/YCmocy9ArjVAP/BeXyTtADTBBX6V0e5UMqwO8CdQ0kzjb6zu5P1qMzsScCMRvE9OlVg==", + "dev": true, + "dependencies": { + "@rollup/pluginutils": "^3.1.0", + "@types/resolve": "1.17.1", + "builtin-modules": "^3.1.0", + "deepmerge": "^4.2.2", + "is-module": "^1.0.0", + "resolve": "^1.19.0" + }, + "engines": { + "node": ">= 10.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0" + } + }, + "node_modules/@rollup/plugin-replace": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@rollup/plugin-replace/-/plugin-replace-3.0.0.tgz", + "integrity": "sha512-3c7JCbMuYXM4PbPWT4+m/4Y6U60SgsnDT/cCyAyUKwFHg7pTSfsSQzIpETha3a3ig6OdOKzZz87D9ZXIK3qsDg==", + "dev": true, + "dependencies": { + "@rollup/pluginutils": "^3.1.0", + "magic-string": "^0.25.7" + }, + "peerDependencies": { + "rollup": "^1.20.0 || ^2.0.0" + } + }, + "node_modules/@rollup/pluginutils": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-3.1.0.tgz", + "integrity": "sha512-GksZ6pr6TpIjHm8h9lSQ8pi8BE9VeubNT0OMJ3B5uZJ8pz73NPiqOtCog/x2/QzM1ENChPKxMDhiQuRHsqc+lg==", + "dev": true, + "dependencies": { + "@types/estree": "0.0.39", + "estree-walker": "^1.0.1", + "picomatch": "^2.2.2" + }, + "engines": { + "node": ">= 8.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0" + } + }, + "node_modules/@rollup/pluginutils/node_modules/estree-walker": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-1.0.1.tgz", + "integrity": "sha512-1fMXF3YP4pZZVozF8j/ZLfvnR8NSIljt56UhbZ5PeeDmmGHpgpdwQt7ITlGvYaQukCvuBRMLEiKiYC+oeIg4cg==", + "dev": true + }, + "node_modules/@types/estree": { + "version": "0.0.39", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.39.tgz", + "integrity": "sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw==", + "dev": true + }, + "node_modules/@types/node": { + "version": "16.11.6", + "resolved": "https://registry.npmjs.org/@types/node/-/node-16.11.6.tgz", + "integrity": "sha512-ua7PgUoeQFjmWPcoo9khiPum3Pd60k4/2ZGXt18sm2Slk0W0xZTqt5Y0Ny1NyBiN1EVQ/+FaF9NcY4Qe6rwk5w==", + "dev": true + }, + "node_modules/@types/pug": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@types/pug/-/pug-2.0.5.tgz", + "integrity": "sha512-LOnASQoeNZMkzexRuyqcBBDZ6rS+rQxUMkmj5A0PkhhiSZivLIuz6Hxyr1mkGoEZEkk66faROmpMi4fFkrKsBA==", + "dev": true + }, + "node_modules/@types/resolve": { + "version": "1.17.1", + "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.17.1.tgz", + "integrity": "sha512-yy7HuzQhj0dhGpD8RLXSZWEkLsV9ibvxvi6EiJ3bkqLAO1RGo0WbkWQiwpRlSFymTJRz0d3k5LM3kkx8ArDbLw==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/sass": { + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/@types/sass/-/sass-1.16.1.tgz", + "integrity": "sha512-iZUcRrGuz/Tbg3loODpW7vrQJkUtpY2fFSf4ELqqkApcS2TkZ1msk7ie8iZPB86lDOP8QOTTmuvWjc5S0R9OjQ==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/acorn": { + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", + "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/anymatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.2.tgz", + "integrity": "sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==", + "dev": true, + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/axios": { + "version": "0.21.4", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.21.4.tgz", + "integrity": "sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==", + "dev": true, + "dependencies": { + "follow-redirects": "^1.14.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/binary-extensions": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", + "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/bootstrap": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-5.1.3.tgz", + "integrity": "sha512-fcQztozJ8jToQWXxVuEyXWW+dSo8AiXWKwiSSrKWsRB/Qt+Ewwza+JWoLKiTuQLaEPhdNAJ7+Dosc9DOIqNy7Q==", + "dev": true, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/bootstrap" + }, + "peerDependencies": { + "@popperjs/core": "^2.10.2" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "dependencies": { + "fill-range": "^7.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha1-DTM+PwDqxQqhRUq9MO+MKl2ackI=", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true + }, + "node_modules/builtin-modules": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-3.2.0.tgz", + "integrity": "sha512-lGzLKcioL90C7wMczpkY0n/oART3MbBa8R9OFGE1rJxoVI86u4WAGfEk8Wjv10eKSyTHVGkSo3bvBylCEtk7LA==", + "dev": true, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/chokidar": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.2.tgz", + "integrity": "sha512-ekGhOnNVPgT77r4K/U3GDhu+FQ2S8TnK/s2KbIGXi0SZWuwkZ2QNyfWdZW+TVfn84DpEP7rLeCt2UI6bJ8GwbQ==", + "dev": true, + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", + "dev": true + }, + "node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true + }, + "node_modules/commondir": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", + "integrity": "sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs=", + "dev": true + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", + "dev": true + }, + "node_modules/console-clear": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/console-clear/-/console-clear-1.1.1.tgz", + "integrity": "sha512-pMD+MVR538ipqkG5JXeOEbKWS5um1H4LUUccUQG68qpeqBYbzYy79Gh55jkd2TtPdRfUaLWdv6LPP//5Zt0aPQ==", + "engines": { + "node": ">=4" + } + }, + "node_modules/core-js": { + "version": "3.19.0", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.19.0.tgz", + "integrity": "sha512-L1TpFRWXZ76vH1yLM+z6KssLZrP8Z6GxxW4auoCj+XiViOzNPJCAuTIkn03BGdFe6Z5clX5t64wRIRypsZQrUg==", + "hasInstallScript": true, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, + "node_modules/crypto-js": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.1.1.tgz", + "integrity": "sha512-o2JlM7ydqd3Qk9CA0L4NL6mTzU2sdx96a+oOfPu8Mkl/PK51vSyoi8/rQ8NknZtk44vq15lmhAj9CIAGwgeWKw==" + }, + "node_modules/dedent-js": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dedent-js/-/dedent-js-1.0.1.tgz", + "integrity": "sha1-vuX7fJ5yfYXf+iRZDRDsGrElUwU=", + "dev": true + }, + "node_modules/deepmerge": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.2.2.tgz", + "integrity": "sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/detect-indent": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/detect-indent/-/detect-indent-6.1.0.tgz", + "integrity": "sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/dotenv": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-10.0.0.tgz", + "integrity": "sha512-rlBi9d8jpv9Sf1klPjNfFAuWDjKLwTIJJ/VxtoTwIR6hnZxcEOQCZg2oIL3MWBYw5GpUDKOEnND7LXTbIpQ03Q==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/es6-promise": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-3.3.1.tgz", + "integrity": "sha1-oIzd6EzNvzTQJ6FFG8kdS80ophM=", + "dev": true + }, + "node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", + "dev": true, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "dev": true + }, + "node_modules/fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/follow-redirects": { + "version": "1.14.4", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.4.tgz", + "integrity": "sha512-zwGkiSXC1MUJG/qmeIFH2HBJx9u0V46QGUe3YR1fXG8bXQxq7fLj0RjLZQ5nubr9qNJUZrH+xUcwXEoXNpfS+g==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", + "dev": true + }, + "node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", + "dev": true + }, + "node_modules/get-port": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/get-port/-/get-port-3.2.0.tgz", + "integrity": "sha1-3Xzn3hh8Bsi/NTeWrHHgmfCYDrw=", + "engines": { + "node": ">=4" + } + }, + "node_modules/glob": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz", + "integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.8", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.8.tgz", + "integrity": "sha512-qkIilPUYcNhJpd33n0GBXTB1MMPp14TxEsEs0pTrsSVucApsYzW5V+Q8Qxhik6KU3evy+qkAAowTByymK0avdg==", + "dev": true + }, + "node_modules/has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "dev": true, + "dependencies": { + "function-bind": "^1.1.1" + }, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/import-cwd": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/import-cwd/-/import-cwd-3.0.0.tgz", + "integrity": "sha512-4pnzH16plW+hgvRECbDWpQl3cqtvSofHWh44met7ESfZ8UZOWWddm8hEyDTqREJ9RbYHY8gi8DqmaelApoOGMg==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "import-from": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/import-from": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/import-from/-/import-from-3.0.0.tgz", + "integrity": "sha512-CiuXOFFSzkU5x/CR0+z7T91Iht4CXgfCxVOFRhh2Zyhg5wOpWvvDLQUsWl+gcN+QscYBjez8hDCt85O7RLDttQ==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "dev": true, + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-core-module": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.8.0.tgz", + "integrity": "sha512-vd15qHsaqrRL7dtH6QNuy0ndJmRDrS9HAM1CAiSifNUFv4x1a0CCVsj18hJ1mShxIG6T2i1sO78MkP56r0nYRw==", + "dev": true, + "dependencies": { + "has": "^1.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-module": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz", + "integrity": "sha1-Mlj7afeMFNW4FdZkM2tM/7ZEFZE=", + "dev": true + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-reference": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-1.2.1.tgz", + "integrity": "sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ==", + "dev": true, + "dependencies": { + "@types/estree": "*" + } + }, + "node_modules/jest-worker": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-26.6.2.tgz", + "integrity": "sha512-KWYVV1c4i+jbMpaBC+U++4Va0cp8OisU185o73T1vo99hqi7w8tSJfUXYswwqqrjzwxa6KpRK54WhPvwf5w6PQ==", + "dev": true, + "dependencies": { + "@types/node": "*", + "merge-stream": "^2.0.0", + "supports-color": "^7.0.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/jest-worker/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-worker/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true + }, + "node_modules/kleur": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", + "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", + "engines": { + "node": ">=6" + } + }, + "node_modules/lilconfig": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.0.3.tgz", + "integrity": "sha512-EHKqr/+ZvdKCifpNrJCKxBTgk5XupZA3y/aCPY9mxfgBzmgh93Mt/WqjjQ38oMxXuvDokaKiM3lAgvSH2sjtHg==", + "dev": true, + "optional": true, + "peer": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/livereload": { + "version": "0.9.3", + "resolved": "https://registry.npmjs.org/livereload/-/livereload-0.9.3.tgz", + "integrity": "sha512-q7Z71n3i4X0R9xthAryBdNGVGAO2R5X+/xXpmKeuPMrteg+W2U8VusTKV3YiJbXZwKsOlFlHe+go6uSNjfxrZw==", + "dev": true, + "dependencies": { + "chokidar": "^3.5.0", + "livereload-js": "^3.3.1", + "opts": ">= 1.2.0", + "ws": "^7.4.3" + }, + "bin": { + "livereload": "bin/livereload.js" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/livereload-js": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/livereload-js/-/livereload-js-3.3.2.tgz", + "integrity": "sha512-w677WnINxFkuixAoUEXOStewzLYGI76XVag+0JWMMEyjJQKs0ibWZMxkTlB96Lm3EjZ7IeOxVziBEbtxVQqQZA==", + "dev": true + }, + "node_modules/local-access": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/local-access/-/local-access-1.1.0.tgz", + "integrity": "sha512-XfegD5pyTAfb+GY6chk283Ox5z8WexG56OvM06RWLpAc/UHozO8X6xAxEkIitZOtsSMM1Yr3DkHgW5W+onLhCw==", + "engines": { + "node": ">=6" + } + }, + "node_modules/lower-case": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-2.0.2.tgz", + "integrity": "sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==", + "dev": true, + "dependencies": { + "tslib": "^2.0.3" + } + }, + "node_modules/magic-string": { + "version": "0.25.7", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.7.tgz", + "integrity": "sha512-4CrMT5DOHTDk4HYDlzmwu4FVCcIYI8gauveasrdCu2IKIFOJ3f0v/8MDGJCDL9oD2ppz/Av1b0Nj345H9M+XIA==", + "dev": true, + "dependencies": { + "sourcemap-codec": "^1.4.4" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true + }, + "node_modules/mime": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.5.2.tgz", + "integrity": "sha512-tqkh47FzKeCPD2PUiPB6pkbMzsCasjxAfC62/Wap5qrUWcb+sFasXUC5I3gYM5iBM8v/Qpn4UK0x+j0iHyFPDg==", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/min-indent": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", + "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", + "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==", + "dev": true + }, + "node_modules/mkdirp": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz", + "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==", + "dev": true, + "dependencies": { + "minimist": "^1.2.5" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, + "node_modules/mri": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz", + "integrity": "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==", + "engines": { + "node": ">=4" + } + }, + "node_modules/nanoid": { + "version": "3.1.30", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.1.30.tgz", + "integrity": "sha512-zJpuPDwOv8D2zq2WRoMe1HsfZthVewpel9CAvTfc/2mBD1uUT/agc5f7GHGWXlYkFvi1mVxe4IjvP2HNrop7nQ==", + "dev": true, + "optional": true, + "peer": true, + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/no-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/no-case/-/no-case-3.0.4.tgz", + "integrity": "sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==", + "dev": true, + "dependencies": { + "lower-case": "^2.0.2", + "tslib": "^2.0.3" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/oidc-client": { + "version": "1.11.5", + "resolved": "https://registry.npmjs.org/oidc-client/-/oidc-client-1.11.5.tgz", + "integrity": "sha512-LcKrKC8Av0m/KD/4EFmo9Sg8fSQ+WFJWBrmtWd+tZkNn3WT/sQG3REmPANE9tzzhbjW6VkTNy4xhAXCfPApAOg==", + "dependencies": { + "acorn": "^7.4.1", + "base64-js": "^1.5.1", + "core-js": "^3.8.3", + "crypto-js": "^4.0.0", + "serialize-javascript": "^4.0.0" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "dev": true, + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/opts": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/opts/-/opts-2.0.2.tgz", + "integrity": "sha512-k41FwbcLnlgnFh69f4qdUfvDQ+5vaSDnVPFI/y5XuhKRq97EnVVneO9F1ESVCdiVu4fCS2L8usX3mU331hB7pg==", + "dev": true + }, + "node_modules/pascal-case": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/pascal-case/-/pascal-case-3.1.2.tgz", + "integrity": "sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g==", + "dev": true, + "dependencies": { + "no-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true + }, + "node_modules/picocolors": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", + "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", + "dev": true, + "optional": true, + "peer": true + }, + "node_modules/picomatch": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.0.tgz", + "integrity": "sha512-lY1Q/PiJGC2zOv/z391WOTD+Z02bCgsFfvxoXXf6h7kv9o+WmsmzYqrAwY63sNgOxE4xEdq0WyUnXfKeBrSvYw==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.3.11", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.3.11.tgz", + "integrity": "sha512-hCmlUAIlUiav8Xdqw3Io4LcpA1DOt7h3LSTAC4G6JGHFFaWzI6qvFt9oilvl8BmkbBRX1IhM90ZAmpk68zccQA==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "nanoid": "^3.1.30", + "picocolors": "^1.0.0", + "source-map-js": "^0.6.2" + }, + "engines": { + "node": "^10 || ^12 || >=14" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + } + }, + "node_modules/postcss-load-config": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-3.1.0.tgz", + "integrity": "sha512-ipM8Ds01ZUophjDTQYSVP70slFSYg3T0/zyfII5vzhN6V57YSxMgG5syXuwi5VtS8wSf3iL30v0uBdoIVx4Q0g==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "import-cwd": "^3.0.0", + "lilconfig": "^2.0.3", + "yaml": "^1.10.2" + }, + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + "peerDependencies": { + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "ts-node": { + "optional": true + } + } + }, + "node_modules/randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "dependencies": { + "safe-buffer": "^5.1.0" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/require-relative": { + "version": "0.8.7", + "resolved": "https://registry.npmjs.org/require-relative/-/require-relative-0.8.7.tgz", + "integrity": "sha1-eZlTn8ngR6N5KPoZb44VY9q9Nt4=", + "dev": true + }, + "node_modules/resolve": { + "version": "1.20.0", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.20.0.tgz", + "integrity": "sha512-wENBPt4ySzg4ybFQW2TT1zMQucPK95HSh/nq2CFTZVOGut2+pQvSsgtda4d26YrYcr067wjbmzOG8byDPBX63A==", + "dev": true, + "dependencies": { + "is-core-module": "^2.2.0", + "path-parse": "^1.0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "optional": true, + "peer": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/rimraf": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", + "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "dev": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + } + }, + "node_modules/rollup": { + "version": "2.58.3", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.58.3.tgz", + "integrity": "sha512-ei27MSw1KhRur4p87Q0/Va2NAYqMXOX++FNEumMBcdreIRLURKy+cE2wcDJKBn0nfmhP2ZGrJkP1XPO+G8FJQw==", + "dev": true, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=10.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/rollup-plugin-css-only": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/rollup-plugin-css-only/-/rollup-plugin-css-only-3.1.0.tgz", + "integrity": "sha512-TYMOE5uoD76vpj+RTkQLzC9cQtbnJNktHPB507FzRWBVaofg7KhIqq1kGbcVOadARSozWF883Ho9KpSPKH8gqA==", + "dev": true, + "dependencies": { + "@rollup/pluginutils": "4" + }, + "engines": { + "node": ">=10.12.0" + }, + "peerDependencies": { + "rollup": "1 || 2" + } + }, + "node_modules/rollup-plugin-css-only/node_modules/@rollup/pluginutils": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-4.1.1.tgz", + "integrity": "sha512-clDjivHqWGXi7u+0d2r2sBi4Ie6VLEAzWMIkvJLnDmxoOhBYOTfzGbOQBA32THHm11/LiJbd01tJUpJsbshSWQ==", + "dev": true, + "dependencies": { + "estree-walker": "^2.0.1", + "picomatch": "^2.2.2" + }, + "engines": { + "node": ">= 8.0.0" + } + }, + "node_modules/rollup-plugin-json": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/rollup-plugin-json/-/rollup-plugin-json-4.0.0.tgz", + "integrity": "sha512-hgb8N7Cgfw5SZAkb3jf0QXii6QX/FOkiIq2M7BAQIEydjHvTyxXHQiIzZaTFgx1GK0cRCHOCBHIyEkkLdWKxow==", + "deprecated": "This module has been deprecated and is no longer maintained. Please use @rollup/plugin-json.", + "dev": true, + "dependencies": { + "rollup-pluginutils": "^2.5.0" + } + }, + "node_modules/rollup-plugin-livereload": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/rollup-plugin-livereload/-/rollup-plugin-livereload-2.0.5.tgz", + "integrity": "sha512-vqQZ/UQowTW7VoiKEM5ouNW90wE5/GZLfdWuR0ELxyKOJUIaj+uismPZZaICU4DnWPVjnpCDDxEqwU7pcKY/PA==", + "dev": true, + "dependencies": { + "livereload": "^0.9.1" + }, + "engines": { + "node": ">=8.3" + } + }, + "node_modules/rollup-plugin-svelte": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/rollup-plugin-svelte/-/rollup-plugin-svelte-7.1.0.tgz", + "integrity": "sha512-vopCUq3G+25sKjwF5VilIbiY6KCuMNHP1PFvx2Vr3REBNMDllKHFZN2B9jwwC+MqNc3UPKkjXnceLPEjTjXGXg==", + "dev": true, + "dependencies": { + "require-relative": "^0.8.7", + "rollup-pluginutils": "^2.8.2" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "rollup": ">=2.0.0", + "svelte": ">=3.5.0" + } + }, + "node_modules/rollup-plugin-terser": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/rollup-plugin-terser/-/rollup-plugin-terser-7.0.2.tgz", + "integrity": "sha512-w3iIaU4OxcF52UUXiZNsNeuXIMDvFrr+ZXK6bFZ0Q60qyVfq4uLptoS4bbq3paG3x216eQllFZX7zt6TIImguQ==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.10.4", + "jest-worker": "^26.2.1", + "serialize-javascript": "^4.0.0", + "terser": "^5.0.0" + }, + "peerDependencies": { + "rollup": "^2.0.0" + } + }, + "node_modules/rollup-pluginutils": { + "version": "2.8.2", + "resolved": "https://registry.npmjs.org/rollup-pluginutils/-/rollup-pluginutils-2.8.2.tgz", + "integrity": "sha512-EEp9NhnUkwY8aif6bxgovPHMoMoNr2FulJziTndpt5H9RdwC47GSGuII9XxpSdzVGM0GWrNPHV6ie1LTNJPaLQ==", + "dev": true, + "dependencies": { + "estree-walker": "^0.6.1" + } + }, + "node_modules/rollup-pluginutils/node_modules/estree-walker": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-0.6.1.tgz", + "integrity": "sha512-SqmZANLWS0mnatqbSfRP5g8OXZC12Fgg1IwNtLsyHDzJizORW4khDfjPqJZsemPWBB2uqykUah5YpQ6epsqC/w==", + "dev": true + }, + "node_modules/sade": { + "version": "1.7.4", + "resolved": "https://registry.npmjs.org/sade/-/sade-1.7.4.tgz", + "integrity": "sha512-y5yauMD93rX840MwUJr7C1ysLFBgMspsdTo4UVrDg3fXDvtwOyIqykhVAAm6fk/3au77773itJStObgK+LKaiA==", + "dependencies": { + "mri": "^1.1.0" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/sander": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/sander/-/sander-0.5.1.tgz", + "integrity": "sha1-dB4kXiMfB8r7b98PEzrfohalAq0=", + "dev": true, + "dependencies": { + "es6-promise": "^3.1.2", + "graceful-fs": "^4.1.3", + "mkdirp": "^0.5.1", + "rimraf": "^2.5.2" + } + }, + "node_modules/sass": { + "version": "1.43.4", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.43.4.tgz", + "integrity": "sha512-/ptG7KE9lxpGSYiXn7Ar+lKOv37xfWsZRtFYal2QHNigyVQDx685VFT/h7ejVr+R8w7H4tmUgtulsKl5YpveOg==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "chokidar": ">=3.0.0 <4.0.0" + }, + "bin": { + "sass": "sass.js" + }, + "engines": { + "node": ">=8.9.0" + } + }, + "node_modules/semiver": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/semiver/-/semiver-1.1.0.tgz", + "integrity": "sha512-QNI2ChmuioGC1/xjyYwyZYADILWyW6AmS1UH6gDj/SFUUUS4MBAWs/7mxnkRPc/F4iHezDP+O8t0dO8WHiEOdg==", + "engines": { + "node": ">=6" + } + }, + "node_modules/serialize-javascript": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-4.0.0.tgz", + "integrity": "sha512-GaNA54380uFefWghODBWEGisLZFj00nS5ACs6yHa9nLqlLpVLO8ChDGeKRjZnV4Nh4n0Qi7nhYZD/9fCPzEqkw==", + "dependencies": { + "randombytes": "^2.1.0" + } + }, + "node_modules/sirv": { + "version": "1.0.18", + "resolved": "https://registry.npmjs.org/sirv/-/sirv-1.0.18.tgz", + "integrity": "sha512-f2AOPogZmXgJ9Ma2M22ZEhc1dNtRIzcEkiflMFeVTRq+OViOZMvH1IPMVOwrKaxpSaHioBJiDR0SluRqGa7atA==", + "dependencies": { + "@polka/url": "^1.0.0-next.20", + "mime": "^2.3.1", + "totalist": "^1.0.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/sirv-cli": { + "version": "1.0.14", + "resolved": "https://registry.npmjs.org/sirv-cli/-/sirv-cli-1.0.14.tgz", + "integrity": "sha512-yyUTNr984ANKDloqepkYbBSqvx3buwYg2sQKPWjSU+IBia5loaoka2If8N9CMwt8AfP179cdEl7kYJ//iWJHjQ==", + "dependencies": { + "console-clear": "^1.1.0", + "get-port": "^3.2.0", + "kleur": "^3.0.0", + "local-access": "^1.0.1", + "sade": "^1.6.0", + "semiver": "^1.0.0", + "sirv": "^1.0.13", + "tinydate": "^1.0.0" + }, + "bin": { + "sirv": "bin.js" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/sorcery": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/sorcery/-/sorcery-0.10.0.tgz", + "integrity": "sha1-iukK19fLBfxZ8asMY3hF1cFaUrc=", + "dev": true, + "dependencies": { + "buffer-crc32": "^0.2.5", + "minimist": "^1.2.0", + "sander": "^0.5.0", + "sourcemap-codec": "^1.3.0" + }, + "bin": { + "sorcery": "bin/index.js" + } + }, + "node_modules/source-map": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.3.tgz", + "integrity": "sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/source-map-js": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-0.6.2.tgz", + "integrity": "sha512-/3GptzWzu0+0MBQFrDKzw/DvvMTUORvgY6k6jd/VS6iCR4RDTKWH6v6WPwQoUO8667uQEf9Oe38DxAYWY5F/Ug==", + "dev": true, + "optional": true, + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.20", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.20.tgz", + "integrity": "sha512-n1lZZ8Ve4ksRqizaBQgxXDgKwttHDhyfQjA6YZZn8+AroHbsIz+JjwxQDxbp+7y5OYCI8t1Yk7etjD9CRd2hIw==", + "dev": true, + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/source-map-support/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/sourcemap-codec": { + "version": "1.4.8", + "resolved": "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz", + "integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==", + "dev": true + }, + "node_modules/strip-indent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", + "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", + "dev": true, + "dependencies": { + "min-indent": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/svelte": { + "version": "3.44.0", + "resolved": "https://registry.npmjs.org/svelte/-/svelte-3.44.0.tgz", + "integrity": "sha512-zWACSJBSncGiDvFfYOMFGNV5zDLOlyhftmO5yOZ0lEtQMptpElaRtl39MWz1+lYCpwUq4F3Q2lTzI9TrTL+eMA==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/svelte-navigator": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/svelte-navigator/-/svelte-navigator-3.1.5.tgz", + "integrity": "sha512-CGTaexasSLpUaTSN2AlYqii0JeisIgg7uZbm8XCLKlpM9Qv3IltlJ7Nvh90Xw9ND97KqtGOjNJ3LNwMN1ABV0w==", + "dev": true, + "dependencies": { + "svelte2tsx": "^0.1.151" + }, + "peerDependencies": { + "svelte": "3.x" + } + }, + "node_modules/svelte-preprocess": { + "version": "4.9.8", + "resolved": "https://registry.npmjs.org/svelte-preprocess/-/svelte-preprocess-4.9.8.tgz", + "integrity": "sha512-EQS/oRZzMtYdAprppZxY3HcysKh11w54MgA63ybtL+TAZ4hVqYOnhw41JVJjWN9dhPnNjjLzvbZ2tMhTsla1Og==", + "dev": true, + "hasInstallScript": true, + "dependencies": { + "@types/pug": "^2.0.4", + "@types/sass": "^1.16.0", + "detect-indent": "^6.0.0", + "magic-string": "^0.25.7", + "sorcery": "^0.10.0", + "strip-indent": "^3.0.0" + }, + "engines": { + "node": ">= 9.11.2" + }, + "peerDependencies": { + "@babel/core": "^7.10.2", + "coffeescript": "^2.5.1", + "less": "^3.11.3", + "postcss": "^7 || ^8", + "postcss-load-config": "^2.1.0 || ^3.0.0", + "pug": "^3.0.0", + "sass": "^1.26.8", + "stylus": "^0.54.7", + "sugarss": "^2.0.0", + "svelte": "^3.23.0", + "typescript": "^3.9.5 || ^4.0.0" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + }, + "coffeescript": { + "optional": true + }, + "less": { + "optional": true + }, + "node-sass": { + "optional": true + }, + "postcss": { + "optional": true + }, + "postcss-load-config": { + "optional": true + }, + "pug": { + "optional": true + }, + "sass": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "typescript": { + "optional": true + } + } + }, + "node_modules/svelte2tsx": { + "version": "0.1.193", + "resolved": "https://registry.npmjs.org/svelte2tsx/-/svelte2tsx-0.1.193.tgz", + "integrity": "sha512-vzy4YQNYDnoqp2iZPnJy7kpPAY6y121L0HKrSBjU/IWW7DQ6T7RMJed2VVHFmVYm0zAGYMDl9urPc6R4DDUyhg==", + "dev": true, + "dependencies": { + "dedent-js": "^1.0.1", + "pascal-case": "^3.1.1" + }, + "peerDependencies": { + "svelte": "^3.24", + "typescript": "^4.1.2" + } + }, + "node_modules/terser": { + "version": "5.9.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.9.0.tgz", + "integrity": "sha512-h5hxa23sCdpzcye/7b8YqbE5OwKca/ni0RQz1uRX3tGh8haaGHqcuSqbGRybuAKNdntZ0mDgFNXPJ48xQ2RXKQ==", + "dev": true, + "dependencies": { + "commander": "^2.20.0", + "source-map": "~0.7.2", + "source-map-support": "~0.5.20" + }, + "bin": { + "terser": "bin/terser" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/tinydate": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/tinydate/-/tinydate-1.3.0.tgz", + "integrity": "sha512-7cR8rLy2QhYHpsBDBVYnnWXm8uRTr38RoZakFSW7Bs7PzfMPNZthuMLkwqZv7MTu8lhQ91cOFYS5a7iFj2oR3w==", + "engines": { + "node": ">=4" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/totalist": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/totalist/-/totalist-1.1.0.tgz", + "integrity": "sha512-gduQwd1rOdDMGxFG1gEvhV88Oirdo2p+KjoYFU7k2g+i7n6AFFbDQ5kMPUsW0pNbfQsB/cwXvT1i4Bue0s9g5g==", + "engines": { + "node": ">=6" + } + }, + "node_modules/tslib": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz", + "integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==", + "dev": true + }, + "node_modules/typescript": { + "version": "4.4.4", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.4.4.tgz", + "integrity": "sha512-DqGhF5IKoBl8WNf8C1gu8q0xZSInh9j1kJJMqT3a94w1JzVaBU4EXOSMrz9yDqMT0xt3selp83fuFMQ0uzv6qA==", + "dev": true, + "peer": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=4.2.0" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", + "dev": true + }, + "node_modules/ws": { + "version": "7.5.5", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.5.tgz", + "integrity": "sha512-BAkMFcAzl8as1G/hArkxOxq3G7pjUqQ3gzYbLL0/5zNkph70e+lCoxBGnm6AW1+/aiNeV4fnKqZ8m4GZewmH2w==", + "dev": true, + "engines": { + "node": ">=8.3.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/yaml": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", + "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", + "dev": true, + "optional": true, + "peer": true, + "engines": { + "node": ">= 6" + } + } + }, + "dependencies": { + "@babel/code-frame": { + "version": "7.15.8", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.15.8.tgz", + "integrity": "sha512-2IAnmn8zbvC/jKYhq5Ki9I+DwjlrtMPUCH/CpHvqI4dNnlwHwsxoIhlc8WcYY5LSYknXQtAlFYuHfqAFCvQ4Wg==", + "dev": true, + "requires": { + "@babel/highlight": "^7.14.5" + } + }, + "@babel/helper-validator-identifier": { + "version": "7.15.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.15.7.tgz", + "integrity": "sha512-K4JvCtQqad9OY2+yTU8w+E82ywk/fe+ELNlt1G8z3bVGlZfn/hOcQQsUhGhW/N+tb3fxK800wLtKOE/aM0m72w==", + "dev": true + }, + "@babel/highlight": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.14.5.tgz", + "integrity": "sha512-qf9u2WFWVV0MppaL877j2dBtQIDgmidgjGk5VIMw3OadXvYaXn66U1BFlH2t4+t3i+8PhedppRv+i40ABzd+gg==", + "dev": true, + "requires": { + "@babel/helper-validator-identifier": "^7.14.5", + "chalk": "^2.0.0", + "js-tokens": "^4.0.0" + } + }, + "@polka/url": { + "version": "1.0.0-next.21", + "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.21.tgz", + "integrity": "sha512-a5Sab1C4/icpTZVzZc5Ghpz88yQtGOyNqYXcZgOssB2uuAr+wF/MvN6bgtW32q7HHrvBki+BsZ0OuNv6EV3K9g==" + }, + "@popperjs/core": { + "version": "2.10.2", + "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.10.2.tgz", + "integrity": "sha512-IXf3XA7+XyN7CP9gGh/XB0UxVMlvARGEgGXLubFICsUMGz6Q+DU+i4gGlpOxTjKvXjkJDJC8YdqdKkDj9qZHEQ==", + "dev": true, + "peer": true + }, + "@rollup/plugin-commonjs": { + "version": "17.1.0", + "resolved": "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-17.1.0.tgz", + "integrity": "sha512-PoMdXCw0ZyvjpCMT5aV4nkL0QywxP29sODQsSGeDpr/oI49Qq9tRtAsb/LbYbDzFlOydVEqHmmZWFtXJEAX9ew==", + "dev": true, + "requires": { + "@rollup/pluginutils": "^3.1.0", + "commondir": "^1.0.1", + "estree-walker": "^2.0.1", + "glob": "^7.1.6", + "is-reference": "^1.2.1", + "magic-string": "^0.25.7", + "resolve": "^1.17.0" + } + }, + "@rollup/plugin-node-resolve": { + "version": "11.2.1", + "resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-11.2.1.tgz", + "integrity": "sha512-yc2n43jcqVyGE2sqV5/YCmocy9ArjVAP/BeXyTtADTBBX6V0e5UMqwO8CdQ0kzjb6zu5P1qMzsScCMRvE9OlVg==", + "dev": true, + "requires": { + "@rollup/pluginutils": "^3.1.0", + "@types/resolve": "1.17.1", + "builtin-modules": "^3.1.0", + "deepmerge": "^4.2.2", + "is-module": "^1.0.0", + "resolve": "^1.19.0" + } + }, + "@rollup/plugin-replace": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@rollup/plugin-replace/-/plugin-replace-3.0.0.tgz", + "integrity": "sha512-3c7JCbMuYXM4PbPWT4+m/4Y6U60SgsnDT/cCyAyUKwFHg7pTSfsSQzIpETha3a3ig6OdOKzZz87D9ZXIK3qsDg==", + "dev": true, + "requires": { + "@rollup/pluginutils": "^3.1.0", + "magic-string": "^0.25.7" + } + }, + "@rollup/pluginutils": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-3.1.0.tgz", + "integrity": "sha512-GksZ6pr6TpIjHm8h9lSQ8pi8BE9VeubNT0OMJ3B5uZJ8pz73NPiqOtCog/x2/QzM1ENChPKxMDhiQuRHsqc+lg==", + "dev": true, + "requires": { + "@types/estree": "0.0.39", + "estree-walker": "^1.0.1", + "picomatch": "^2.2.2" + }, + "dependencies": { + "estree-walker": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-1.0.1.tgz", + "integrity": "sha512-1fMXF3YP4pZZVozF8j/ZLfvnR8NSIljt56UhbZ5PeeDmmGHpgpdwQt7ITlGvYaQukCvuBRMLEiKiYC+oeIg4cg==", + "dev": true + } + } + }, + "@types/estree": { + "version": "0.0.39", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.39.tgz", + "integrity": "sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw==", + "dev": true + }, + "@types/node": { + "version": "16.11.6", + "resolved": "https://registry.npmjs.org/@types/node/-/node-16.11.6.tgz", + "integrity": "sha512-ua7PgUoeQFjmWPcoo9khiPum3Pd60k4/2ZGXt18sm2Slk0W0xZTqt5Y0Ny1NyBiN1EVQ/+FaF9NcY4Qe6rwk5w==", + "dev": true + }, + "@types/pug": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@types/pug/-/pug-2.0.5.tgz", + "integrity": "sha512-LOnASQoeNZMkzexRuyqcBBDZ6rS+rQxUMkmj5A0PkhhiSZivLIuz6Hxyr1mkGoEZEkk66faROmpMi4fFkrKsBA==", + "dev": true + }, + "@types/resolve": { + "version": "1.17.1", + "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.17.1.tgz", + "integrity": "sha512-yy7HuzQhj0dhGpD8RLXSZWEkLsV9ibvxvi6EiJ3bkqLAO1RGo0WbkWQiwpRlSFymTJRz0d3k5LM3kkx8ArDbLw==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, + "@types/sass": { + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/@types/sass/-/sass-1.16.1.tgz", + "integrity": "sha512-iZUcRrGuz/Tbg3loODpW7vrQJkUtpY2fFSf4ELqqkApcS2TkZ1msk7ie8iZPB86lDOP8QOTTmuvWjc5S0R9OjQ==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, + "acorn": { + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", + "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==" + }, + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "requires": { + "color-convert": "^1.9.0" + } + }, + "anymatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.2.tgz", + "integrity": "sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==", + "dev": true, + "requires": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + } + }, + "axios": { + "version": "0.21.4", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.21.4.tgz", + "integrity": "sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==", + "dev": true, + "requires": { + "follow-redirects": "^1.14.0" + } + }, + "balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true + }, + "base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==" + }, + "binary-extensions": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", + "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", + "dev": true + }, + "bootstrap": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-5.1.3.tgz", + "integrity": "sha512-fcQztozJ8jToQWXxVuEyXWW+dSo8AiXWKwiSSrKWsRB/Qt+Ewwza+JWoLKiTuQLaEPhdNAJ7+Dosc9DOIqNy7Q==", + "dev": true, + "requires": {} + }, + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "requires": { + "fill-range": "^7.0.1" + } + }, + "buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha1-DTM+PwDqxQqhRUq9MO+MKl2ackI=", + "dev": true + }, + "buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true + }, + "builtin-modules": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-3.2.0.tgz", + "integrity": "sha512-lGzLKcioL90C7wMczpkY0n/oART3MbBa8R9OFGE1rJxoVI86u4WAGfEk8Wjv10eKSyTHVGkSo3bvBylCEtk7LA==", + "dev": true + }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "chokidar": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.2.tgz", + "integrity": "sha512-ekGhOnNVPgT77r4K/U3GDhu+FQ2S8TnK/s2KbIGXi0SZWuwkZ2QNyfWdZW+TVfn84DpEP7rLeCt2UI6bJ8GwbQ==", + "dev": true, + "requires": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "fsevents": "~2.3.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + } + }, + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", + "dev": true + }, + "commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true + }, + "commondir": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", + "integrity": "sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs=", + "dev": true + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", + "dev": true + }, + "console-clear": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/console-clear/-/console-clear-1.1.1.tgz", + "integrity": "sha512-pMD+MVR538ipqkG5JXeOEbKWS5um1H4LUUccUQG68qpeqBYbzYy79Gh55jkd2TtPdRfUaLWdv6LPP//5Zt0aPQ==" + }, + "core-js": { + "version": "3.19.0", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.19.0.tgz", + "integrity": "sha512-L1TpFRWXZ76vH1yLM+z6KssLZrP8Z6GxxW4auoCj+XiViOzNPJCAuTIkn03BGdFe6Z5clX5t64wRIRypsZQrUg==" + }, + "crypto-js": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.1.1.tgz", + "integrity": "sha512-o2JlM7ydqd3Qk9CA0L4NL6mTzU2sdx96a+oOfPu8Mkl/PK51vSyoi8/rQ8NknZtk44vq15lmhAj9CIAGwgeWKw==" + }, + "dedent-js": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dedent-js/-/dedent-js-1.0.1.tgz", + "integrity": "sha1-vuX7fJ5yfYXf+iRZDRDsGrElUwU=", + "dev": true + }, + "deepmerge": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.2.2.tgz", + "integrity": "sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg==", + "dev": true + }, + "detect-indent": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/detect-indent/-/detect-indent-6.1.0.tgz", + "integrity": "sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA==", + "dev": true + }, + "dotenv": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-10.0.0.tgz", + "integrity": "sha512-rlBi9d8jpv9Sf1klPjNfFAuWDjKLwTIJJ/VxtoTwIR6hnZxcEOQCZg2oIL3MWBYw5GpUDKOEnND7LXTbIpQ03Q==", + "dev": true + }, + "es6-promise": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-3.3.1.tgz", + "integrity": "sha1-oIzd6EzNvzTQJ6FFG8kdS80ophM=", + "dev": true + }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", + "dev": true + }, + "estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "dev": true + }, + "fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "requires": { + "to-regex-range": "^5.0.1" + } + }, + "follow-redirects": { + "version": "1.14.4", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.4.tgz", + "integrity": "sha512-zwGkiSXC1MUJG/qmeIFH2HBJx9u0V46QGUe3YR1fXG8bXQxq7fLj0RjLZQ5nubr9qNJUZrH+xUcwXEoXNpfS+g==", + "dev": true + }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", + "dev": true + }, + "fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "optional": true + }, + "function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", + "dev": true + }, + "get-port": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/get-port/-/get-port-3.2.0.tgz", + "integrity": "sha1-3Xzn3hh8Bsi/NTeWrHHgmfCYDrw=" + }, + "glob": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz", + "integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "requires": { + "is-glob": "^4.0.1" + } + }, + "graceful-fs": { + "version": "4.2.8", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.8.tgz", + "integrity": "sha512-qkIilPUYcNhJpd33n0GBXTB1MMPp14TxEsEs0pTrsSVucApsYzW5V+Q8Qxhik6KU3evy+qkAAowTByymK0avdg==", + "dev": true + }, + "has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "dev": true, + "requires": { + "function-bind": "^1.1.1" + } + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "dev": true + }, + "import-cwd": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/import-cwd/-/import-cwd-3.0.0.tgz", + "integrity": "sha512-4pnzH16plW+hgvRECbDWpQl3cqtvSofHWh44met7ESfZ8UZOWWddm8hEyDTqREJ9RbYHY8gi8DqmaelApoOGMg==", + "dev": true, + "optional": true, + "peer": true, + "requires": { + "import-from": "^3.0.0" + } + }, + "import-from": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/import-from/-/import-from-3.0.0.tgz", + "integrity": "sha512-CiuXOFFSzkU5x/CR0+z7T91Iht4CXgfCxVOFRhh2Zyhg5wOpWvvDLQUsWl+gcN+QscYBjez8hDCt85O7RLDttQ==", + "dev": true, + "optional": true, + "peer": true, + "requires": { + "resolve-from": "^5.0.0" + } + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "dev": true, + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true + }, + "is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "requires": { + "binary-extensions": "^2.0.0" + } + }, + "is-core-module": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.8.0.tgz", + "integrity": "sha512-vd15qHsaqrRL7dtH6QNuy0ndJmRDrS9HAM1CAiSifNUFv4x1a0CCVsj18hJ1mShxIG6T2i1sO78MkP56r0nYRw==", + "dev": true, + "requires": { + "has": "^1.0.3" + } + }, + "is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", + "dev": true + }, + "is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "requires": { + "is-extglob": "^2.1.1" + } + }, + "is-module": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz", + "integrity": "sha1-Mlj7afeMFNW4FdZkM2tM/7ZEFZE=", + "dev": true + }, + "is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true + }, + "is-reference": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-1.2.1.tgz", + "integrity": "sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ==", + "dev": true, + "requires": { + "@types/estree": "*" + } + }, + "jest-worker": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-26.6.2.tgz", + "integrity": "sha512-KWYVV1c4i+jbMpaBC+U++4Va0cp8OisU185o73T1vo99hqi7w8tSJfUXYswwqqrjzwxa6KpRK54WhPvwf5w6PQ==", + "dev": true, + "requires": { + "@types/node": "*", + "merge-stream": "^2.0.0", + "supports-color": "^7.0.0" + }, + "dependencies": { + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true + }, + "kleur": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", + "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==" + }, + "lilconfig": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.0.3.tgz", + "integrity": "sha512-EHKqr/+ZvdKCifpNrJCKxBTgk5XupZA3y/aCPY9mxfgBzmgh93Mt/WqjjQ38oMxXuvDokaKiM3lAgvSH2sjtHg==", + "dev": true, + "optional": true, + "peer": true + }, + "livereload": { + "version": "0.9.3", + "resolved": "https://registry.npmjs.org/livereload/-/livereload-0.9.3.tgz", + "integrity": "sha512-q7Z71n3i4X0R9xthAryBdNGVGAO2R5X+/xXpmKeuPMrteg+W2U8VusTKV3YiJbXZwKsOlFlHe+go6uSNjfxrZw==", + "dev": true, + "requires": { + "chokidar": "^3.5.0", + "livereload-js": "^3.3.1", + "opts": ">= 1.2.0", + "ws": "^7.4.3" + } + }, + "livereload-js": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/livereload-js/-/livereload-js-3.3.2.tgz", + "integrity": "sha512-w677WnINxFkuixAoUEXOStewzLYGI76XVag+0JWMMEyjJQKs0ibWZMxkTlB96Lm3EjZ7IeOxVziBEbtxVQqQZA==", + "dev": true + }, + "local-access": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/local-access/-/local-access-1.1.0.tgz", + "integrity": "sha512-XfegD5pyTAfb+GY6chk283Ox5z8WexG56OvM06RWLpAc/UHozO8X6xAxEkIitZOtsSMM1Yr3DkHgW5W+onLhCw==" + }, + "lower-case": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-2.0.2.tgz", + "integrity": "sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==", + "dev": true, + "requires": { + "tslib": "^2.0.3" + } + }, + "magic-string": { + "version": "0.25.7", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.7.tgz", + "integrity": "sha512-4CrMT5DOHTDk4HYDlzmwu4FVCcIYI8gauveasrdCu2IKIFOJ3f0v/8MDGJCDL9oD2ppz/Av1b0Nj345H9M+XIA==", + "dev": true, + "requires": { + "sourcemap-codec": "^1.4.4" + } + }, + "merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true + }, + "mime": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.5.2.tgz", + "integrity": "sha512-tqkh47FzKeCPD2PUiPB6pkbMzsCasjxAfC62/Wap5qrUWcb+sFasXUC5I3gYM5iBM8v/Qpn4UK0x+j0iHyFPDg==" + }, + "min-indent": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", + "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", + "dev": true + }, + "minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "dev": true, + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "minimist": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", + "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==", + "dev": true + }, + "mkdirp": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz", + "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==", + "dev": true, + "requires": { + "minimist": "^1.2.5" + } + }, + "mri": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz", + "integrity": "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==" + }, + "nanoid": { + "version": "3.1.30", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.1.30.tgz", + "integrity": "sha512-zJpuPDwOv8D2zq2WRoMe1HsfZthVewpel9CAvTfc/2mBD1uUT/agc5f7GHGWXlYkFvi1mVxe4IjvP2HNrop7nQ==", + "dev": true, + "optional": true, + "peer": true + }, + "no-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/no-case/-/no-case-3.0.4.tgz", + "integrity": "sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==", + "dev": true, + "requires": { + "lower-case": "^2.0.2", + "tslib": "^2.0.3" + } + }, + "normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true + }, + "oidc-client": { + "version": "1.11.5", + "resolved": "https://registry.npmjs.org/oidc-client/-/oidc-client-1.11.5.tgz", + "integrity": "sha512-LcKrKC8Av0m/KD/4EFmo9Sg8fSQ+WFJWBrmtWd+tZkNn3WT/sQG3REmPANE9tzzhbjW6VkTNy4xhAXCfPApAOg==", + "requires": { + "acorn": "^7.4.1", + "base64-js": "^1.5.1", + "core-js": "^3.8.3", + "crypto-js": "^4.0.0", + "serialize-javascript": "^4.0.0" + } + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "dev": true, + "requires": { + "wrappy": "1" + } + }, + "opts": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/opts/-/opts-2.0.2.tgz", + "integrity": "sha512-k41FwbcLnlgnFh69f4qdUfvDQ+5vaSDnVPFI/y5XuhKRq97EnVVneO9F1ESVCdiVu4fCS2L8usX3mU331hB7pg==", + "dev": true + }, + "pascal-case": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/pascal-case/-/pascal-case-3.1.2.tgz", + "integrity": "sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g==", + "dev": true, + "requires": { + "no-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", + "dev": true + }, + "path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true + }, + "picocolors": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", + "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", + "dev": true, + "optional": true, + "peer": true + }, + "picomatch": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.0.tgz", + "integrity": "sha512-lY1Q/PiJGC2zOv/z391WOTD+Z02bCgsFfvxoXXf6h7kv9o+WmsmzYqrAwY63sNgOxE4xEdq0WyUnXfKeBrSvYw==", + "dev": true + }, + "postcss": { + "version": "8.3.11", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.3.11.tgz", + "integrity": "sha512-hCmlUAIlUiav8Xdqw3Io4LcpA1DOt7h3LSTAC4G6JGHFFaWzI6qvFt9oilvl8BmkbBRX1IhM90ZAmpk68zccQA==", + "dev": true, + "optional": true, + "peer": true, + "requires": { + "nanoid": "^3.1.30", + "picocolors": "^1.0.0", + "source-map-js": "^0.6.2" + } + }, + "postcss-load-config": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-3.1.0.tgz", + "integrity": "sha512-ipM8Ds01ZUophjDTQYSVP70slFSYg3T0/zyfII5vzhN6V57YSxMgG5syXuwi5VtS8wSf3iL30v0uBdoIVx4Q0g==", + "dev": true, + "optional": true, + "peer": true, + "requires": { + "import-cwd": "^3.0.0", + "lilconfig": "^2.0.3", + "yaml": "^1.10.2" + } + }, + "randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "requires": { + "safe-buffer": "^5.1.0" + } + }, + "readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "requires": { + "picomatch": "^2.2.1" + } + }, + "require-relative": { + "version": "0.8.7", + "resolved": "https://registry.npmjs.org/require-relative/-/require-relative-0.8.7.tgz", + "integrity": "sha1-eZlTn8ngR6N5KPoZb44VY9q9Nt4=", + "dev": true + }, + "resolve": { + "version": "1.20.0", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.20.0.tgz", + "integrity": "sha512-wENBPt4ySzg4ybFQW2TT1zMQucPK95HSh/nq2CFTZVOGut2+pQvSsgtda4d26YrYcr067wjbmzOG8byDPBX63A==", + "dev": true, + "requires": { + "is-core-module": "^2.2.0", + "path-parse": "^1.0.6" + } + }, + "resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "optional": true, + "peer": true + }, + "rimraf": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", + "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "dev": true, + "requires": { + "glob": "^7.1.3" + } + }, + "rollup": { + "version": "2.58.3", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.58.3.tgz", + "integrity": "sha512-ei27MSw1KhRur4p87Q0/Va2NAYqMXOX++FNEumMBcdreIRLURKy+cE2wcDJKBn0nfmhP2ZGrJkP1XPO+G8FJQw==", + "dev": true, + "requires": { + "fsevents": "~2.3.2" + } + }, + "rollup-plugin-css-only": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/rollup-plugin-css-only/-/rollup-plugin-css-only-3.1.0.tgz", + "integrity": "sha512-TYMOE5uoD76vpj+RTkQLzC9cQtbnJNktHPB507FzRWBVaofg7KhIqq1kGbcVOadARSozWF883Ho9KpSPKH8gqA==", + "dev": true, + "requires": { + "@rollup/pluginutils": "4" + }, + "dependencies": { + "@rollup/pluginutils": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-4.1.1.tgz", + "integrity": "sha512-clDjivHqWGXi7u+0d2r2sBi4Ie6VLEAzWMIkvJLnDmxoOhBYOTfzGbOQBA32THHm11/LiJbd01tJUpJsbshSWQ==", + "dev": true, + "requires": { + "estree-walker": "^2.0.1", + "picomatch": "^2.2.2" + } + } + } + }, + "rollup-plugin-json": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/rollup-plugin-json/-/rollup-plugin-json-4.0.0.tgz", + "integrity": "sha512-hgb8N7Cgfw5SZAkb3jf0QXii6QX/FOkiIq2M7BAQIEydjHvTyxXHQiIzZaTFgx1GK0cRCHOCBHIyEkkLdWKxow==", + "dev": true, + "requires": { + "rollup-pluginutils": "^2.5.0" + } + }, + "rollup-plugin-livereload": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/rollup-plugin-livereload/-/rollup-plugin-livereload-2.0.5.tgz", + "integrity": "sha512-vqQZ/UQowTW7VoiKEM5ouNW90wE5/GZLfdWuR0ELxyKOJUIaj+uismPZZaICU4DnWPVjnpCDDxEqwU7pcKY/PA==", + "dev": true, + "requires": { + "livereload": "^0.9.1" + } + }, + "rollup-plugin-svelte": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/rollup-plugin-svelte/-/rollup-plugin-svelte-7.1.0.tgz", + "integrity": "sha512-vopCUq3G+25sKjwF5VilIbiY6KCuMNHP1PFvx2Vr3REBNMDllKHFZN2B9jwwC+MqNc3UPKkjXnceLPEjTjXGXg==", + "dev": true, + "requires": { + "require-relative": "^0.8.7", + "rollup-pluginutils": "^2.8.2" + } + }, + "rollup-plugin-terser": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/rollup-plugin-terser/-/rollup-plugin-terser-7.0.2.tgz", + "integrity": "sha512-w3iIaU4OxcF52UUXiZNsNeuXIMDvFrr+ZXK6bFZ0Q60qyVfq4uLptoS4bbq3paG3x216eQllFZX7zt6TIImguQ==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.10.4", + "jest-worker": "^26.2.1", + "serialize-javascript": "^4.0.0", + "terser": "^5.0.0" + } + }, + "rollup-pluginutils": { + "version": "2.8.2", + "resolved": "https://registry.npmjs.org/rollup-pluginutils/-/rollup-pluginutils-2.8.2.tgz", + "integrity": "sha512-EEp9NhnUkwY8aif6bxgovPHMoMoNr2FulJziTndpt5H9RdwC47GSGuII9XxpSdzVGM0GWrNPHV6ie1LTNJPaLQ==", + "dev": true, + "requires": { + "estree-walker": "^0.6.1" + }, + "dependencies": { + "estree-walker": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-0.6.1.tgz", + "integrity": "sha512-SqmZANLWS0mnatqbSfRP5g8OXZC12Fgg1IwNtLsyHDzJizORW4khDfjPqJZsemPWBB2uqykUah5YpQ6epsqC/w==", + "dev": true + } + } + }, + "sade": { + "version": "1.7.4", + "resolved": "https://registry.npmjs.org/sade/-/sade-1.7.4.tgz", + "integrity": "sha512-y5yauMD93rX840MwUJr7C1ysLFBgMspsdTo4UVrDg3fXDvtwOyIqykhVAAm6fk/3au77773itJStObgK+LKaiA==", + "requires": { + "mri": "^1.1.0" + } + }, + "safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" + }, + "sander": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/sander/-/sander-0.5.1.tgz", + "integrity": "sha1-dB4kXiMfB8r7b98PEzrfohalAq0=", + "dev": true, + "requires": { + "es6-promise": "^3.1.2", + "graceful-fs": "^4.1.3", + "mkdirp": "^0.5.1", + "rimraf": "^2.5.2" + } + }, + "sass": { + "version": "1.43.4", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.43.4.tgz", + "integrity": "sha512-/ptG7KE9lxpGSYiXn7Ar+lKOv37xfWsZRtFYal2QHNigyVQDx685VFT/h7ejVr+R8w7H4tmUgtulsKl5YpveOg==", + "dev": true, + "optional": true, + "peer": true, + "requires": { + "chokidar": ">=3.0.0 <4.0.0" + } + }, + "semiver": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/semiver/-/semiver-1.1.0.tgz", + "integrity": "sha512-QNI2ChmuioGC1/xjyYwyZYADILWyW6AmS1UH6gDj/SFUUUS4MBAWs/7mxnkRPc/F4iHezDP+O8t0dO8WHiEOdg==" + }, + "serialize-javascript": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-4.0.0.tgz", + "integrity": "sha512-GaNA54380uFefWghODBWEGisLZFj00nS5ACs6yHa9nLqlLpVLO8ChDGeKRjZnV4Nh4n0Qi7nhYZD/9fCPzEqkw==", + "requires": { + "randombytes": "^2.1.0" + } + }, + "sirv": { + "version": "1.0.18", + "resolved": "https://registry.npmjs.org/sirv/-/sirv-1.0.18.tgz", + "integrity": "sha512-f2AOPogZmXgJ9Ma2M22ZEhc1dNtRIzcEkiflMFeVTRq+OViOZMvH1IPMVOwrKaxpSaHioBJiDR0SluRqGa7atA==", + "requires": { + "@polka/url": "^1.0.0-next.20", + "mime": "^2.3.1", + "totalist": "^1.0.0" + } + }, + "sirv-cli": { + "version": "1.0.14", + "resolved": "https://registry.npmjs.org/sirv-cli/-/sirv-cli-1.0.14.tgz", + "integrity": "sha512-yyUTNr984ANKDloqepkYbBSqvx3buwYg2sQKPWjSU+IBia5loaoka2If8N9CMwt8AfP179cdEl7kYJ//iWJHjQ==", + "requires": { + "console-clear": "^1.1.0", + "get-port": "^3.2.0", + "kleur": "^3.0.0", + "local-access": "^1.0.1", + "sade": "^1.6.0", + "semiver": "^1.0.0", + "sirv": "^1.0.13", + "tinydate": "^1.0.0" + } + }, + "sorcery": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/sorcery/-/sorcery-0.10.0.tgz", + "integrity": "sha1-iukK19fLBfxZ8asMY3hF1cFaUrc=", + "dev": true, + "requires": { + "buffer-crc32": "^0.2.5", + "minimist": "^1.2.0", + "sander": "^0.5.0", + "sourcemap-codec": "^1.3.0" + } + }, + "source-map": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.3.tgz", + "integrity": "sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==", + "dev": true + }, + "source-map-js": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-0.6.2.tgz", + "integrity": "sha512-/3GptzWzu0+0MBQFrDKzw/DvvMTUORvgY6k6jd/VS6iCR4RDTKWH6v6WPwQoUO8667uQEf9Oe38DxAYWY5F/Ug==", + "dev": true, + "optional": true, + "peer": true + }, + "source-map-support": { + "version": "0.5.20", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.20.tgz", + "integrity": "sha512-n1lZZ8Ve4ksRqizaBQgxXDgKwttHDhyfQjA6YZZn8+AroHbsIz+JjwxQDxbp+7y5OYCI8t1Yk7etjD9CRd2hIw==", + "dev": true, + "requires": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + }, + "dependencies": { + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + } + } + }, + "sourcemap-codec": { + "version": "1.4.8", + "resolved": "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz", + "integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==", + "dev": true + }, + "strip-indent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", + "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", + "dev": true, + "requires": { + "min-indent": "^1.0.0" + } + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + }, + "svelte": { + "version": "3.44.0", + "resolved": "https://registry.npmjs.org/svelte/-/svelte-3.44.0.tgz", + "integrity": "sha512-zWACSJBSncGiDvFfYOMFGNV5zDLOlyhftmO5yOZ0lEtQMptpElaRtl39MWz1+lYCpwUq4F3Q2lTzI9TrTL+eMA==", + "dev": true + }, + "svelte-navigator": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/svelte-navigator/-/svelte-navigator-3.1.5.tgz", + "integrity": "sha512-CGTaexasSLpUaTSN2AlYqii0JeisIgg7uZbm8XCLKlpM9Qv3IltlJ7Nvh90Xw9ND97KqtGOjNJ3LNwMN1ABV0w==", + "dev": true, + "requires": { + "svelte2tsx": "^0.1.151" + } + }, + "svelte-preprocess": { + "version": "4.9.8", + "resolved": "https://registry.npmjs.org/svelte-preprocess/-/svelte-preprocess-4.9.8.tgz", + "integrity": "sha512-EQS/oRZzMtYdAprppZxY3HcysKh11w54MgA63ybtL+TAZ4hVqYOnhw41JVJjWN9dhPnNjjLzvbZ2tMhTsla1Og==", + "dev": true, + "requires": { + "@types/pug": "^2.0.4", + "@types/sass": "^1.16.0", + "detect-indent": "^6.0.0", + "magic-string": "^0.25.7", + "sorcery": "^0.10.0", + "strip-indent": "^3.0.0" + } + }, + "svelte2tsx": { + "version": "0.1.193", + "resolved": "https://registry.npmjs.org/svelte2tsx/-/svelte2tsx-0.1.193.tgz", + "integrity": "sha512-vzy4YQNYDnoqp2iZPnJy7kpPAY6y121L0HKrSBjU/IWW7DQ6T7RMJed2VVHFmVYm0zAGYMDl9urPc6R4DDUyhg==", + "dev": true, + "requires": { + "dedent-js": "^1.0.1", + "pascal-case": "^3.1.1" + } + }, + "terser": { + "version": "5.9.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.9.0.tgz", + "integrity": "sha512-h5hxa23sCdpzcye/7b8YqbE5OwKca/ni0RQz1uRX3tGh8haaGHqcuSqbGRybuAKNdntZ0mDgFNXPJ48xQ2RXKQ==", + "dev": true, + "requires": { + "commander": "^2.20.0", + "source-map": "~0.7.2", + "source-map-support": "~0.5.20" + } + }, + "tinydate": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/tinydate/-/tinydate-1.3.0.tgz", + "integrity": "sha512-7cR8rLy2QhYHpsBDBVYnnWXm8uRTr38RoZakFSW7Bs7PzfMPNZthuMLkwqZv7MTu8lhQ91cOFYS5a7iFj2oR3w==" + }, + "to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "requires": { + "is-number": "^7.0.0" + } + }, + "totalist": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/totalist/-/totalist-1.1.0.tgz", + "integrity": "sha512-gduQwd1rOdDMGxFG1gEvhV88Oirdo2p+KjoYFU7k2g+i7n6AFFbDQ5kMPUsW0pNbfQsB/cwXvT1i4Bue0s9g5g==" + }, + "tslib": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz", + "integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==", + "dev": true + }, + "typescript": { + "version": "4.4.4", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.4.4.tgz", + "integrity": "sha512-DqGhF5IKoBl8WNf8C1gu8q0xZSInh9j1kJJMqT3a94w1JzVaBU4EXOSMrz9yDqMT0xt3selp83fuFMQ0uzv6qA==", + "dev": true, + "peer": true + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", + "dev": true + }, + "ws": { + "version": "7.5.5", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.5.tgz", + "integrity": "sha512-BAkMFcAzl8as1G/hArkxOxq3G7pjUqQ3gzYbLL0/5zNkph70e+lCoxBGnm6AW1+/aiNeV4fnKqZ8m4GZewmH2w==", + "dev": true, + "requires": {} + }, + "yaml": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", + "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", + "dev": true, + "optional": true, + "peer": true + } + } +} diff --git a/client/package.json b/client/package.json new file mode 100644 index 0000000..e0d1182 --- /dev/null +++ b/client/package.json @@ -0,0 +1,31 @@ +{ + "name": "svelte-netcore-identity", + "version": "1.0.0", + "private": true, + "scripts": { + "build": "rollup -c", + "dev": "rollup -c -w", + "start": "sirv public --single --no-clear --port 3000" + }, + "devDependencies": { + "@rollup/plugin-commonjs": "^17.0.0", + "@rollup/plugin-node-resolve": "^11.0.0", + "@rollup/plugin-replace": "^3.0.0", + "axios": "^0.21.4", + "bootstrap": "^5.1.3", + "dotenv": "^10.0.0", + "rollup": "^2.3.4", + "rollup-plugin-css-only": "^3.1.0", + "rollup-plugin-json": "^4.0.0", + "rollup-plugin-livereload": "^2.0.0", + "rollup-plugin-svelte": "^7.0.0", + "rollup-plugin-terser": "^7.0.0", + "svelte": "^3.0.0", + "svelte-navigator": "^3.1.5", + "svelte-preprocess": "^4.9.8" + }, + "dependencies": { + "oidc-client": "^1.11.5", + "sirv-cli": "^1.0.14" + } +} diff --git a/client/public/favicon.png b/client/public/favicon.png new file mode 100644 index 0000000..31aed79 Binary files /dev/null and b/client/public/favicon.png differ diff --git a/client/public/global.css b/client/public/global.css new file mode 100644 index 0000000..bb28a94 --- /dev/null +++ b/client/public/global.css @@ -0,0 +1,63 @@ +html, body { + position: relative; + width: 100%; + height: 100%; +} + +body { + color: #333; + margin: 0; + padding: 8px; + box-sizing: border-box; + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif; +} + +a { + color: rgb(0,100,200); + text-decoration: none; +} + +a:hover { + text-decoration: underline; +} + +a:visited { + color: rgb(0,80,160); +} + +label { + display: block; +} + +input, button, select, textarea { + font-family: inherit; + font-size: inherit; + -webkit-padding: 0.4em 0; + padding: 0.4em; + margin: 0 0 0.5em 0; + box-sizing: border-box; + border: 1px solid #ccc; + border-radius: 2px; +} + +input:disabled { + color: #ccc; +} + +button { + color: #333; + background-color: #f4f4f4; + outline: none; +} + +button:disabled { + color: #999; +} + +button:not(:disabled):active { + background-color: #ddd; +} + +button:focus { + border-color: #666; +} diff --git a/client/public/index.html b/client/public/index.html new file mode 100644 index 0000000..a14513b --- /dev/null +++ b/client/public/index.html @@ -0,0 +1,18 @@ + + + + + + + svelte-netcore-identity + + + + + + + + + + + diff --git a/client/public/logo.svg b/client/public/logo.svg new file mode 100644 index 0000000..18b53d1 --- /dev/null +++ b/client/public/logo.svg @@ -0,0 +1,20 @@ + + + + + + + + diff --git a/client/public/mock/stuff.json b/client/public/mock/stuff.json new file mode 100644 index 0000000..318a1b8 --- /dev/null +++ b/client/public/mock/stuff.json @@ -0,0 +1,127 @@ +{ + "page": 1, + "perPage": 6, + "total": 7, + "totalPages": 2, + "datumList": [ + { + "id": "1", + "label": "aaa", + "description": "aaa", + "otherInfo": "aaa", + "createdAt": "1901-03-26T19:18:37.781402Z", + "updatedAt": "1901-03-26T21:03:05.3683671Z", + "user": { + "id": "1", + "name": "Alice Smith", + "givenName": "Alice", + "familyName": "Smith", + "email": "AliceSmith@email.com", + "createdAt": "1901-03-26T20:54:31.7299699Z", + "updatedAt": null + } + }, + { + "id": "2", + "label": "bbb", + "description": "bbb", + "otherInfo": "bbb", + "createdAt": "1901-03-26T19:19:43.9789358Z", + "updatedAt": "1901-03-26T21:02:59.8469004Z", + "user": { + "id": "1", + "name": "Alice Smith", + "givenName": "Alice", + "familyName": "Smith", + "email": "AliceSmith@email.com", + "createdAt": "1901-03-26T20:54:31.7299699Z", + "updatedAt": null + } + }, + { + "id": "3", + "label": "ccc", + "description": "ccc", + "otherInfo": "ccc", + "createdAt": "1901-03-26T21:02:51.9473393Z", + "updatedAt": null, + "user": { + "id": "11", + "name": "Bob Smith", + "givenName": "Bob", + "familyName": "Smith", + "email": "BobSmith@email.com", + "createdAt": "1901-03-26T20:54:31.7299699Z", + "updatedAt": null + } + }, + { + "id": "4", + "label": "ddd", + "description": "ddd", + "otherInfo": "ddd", + "createdAt": "1901-03-26T21:02:32.0019741Z", + "updatedAt": null, + "user": { + "id": "11", + "name": "Bob Smith", + "givenName": "Bob", + "familyName": "Smith", + "email": "BobSmith@email.com", + "createdAt": "1901-03-26T20:54:31.7299699Z", + "updatedAt": null + } + }, + { + "id": "5", + "label": "eee", + "description": "eee", + "otherInfo": "eee", + "createdAt": "1901-03-26T19:08:21.6655156Z", + "updatedAt": null, + "user": { + "id": "11", + "name": "Bob Smith", + "givenName": "Bob", + "familyName": "Smith", + "email": "BobSmith@email.com", + "createdAt": "1901-03-26T20:54:31.7299699Z", + "updatedAt": null + } + }, + { + "id": "6", + "label": "fff", + "description": "fff", + "otherInfo": "fff", + "createdAt": "1901-03-26T18:51:34.588823Z", + "updatedAt": null, + "user": { + "id": "11", + "name": "Bob Smith", + "givenName": "Bob", + "familyName": "Smith", + "email": "BobSmith@email.com", + "createdAt": "1901-03-26T20:54:31.7299699Z", + "updatedAt": null + } + }, + { + "id": "7", + "label": "ggg", + "description": "ggg", + "otherInfo": "ggg", + "createdAt": "1901-03-26T18:51:34.588823Z", + "updatedAt": null, + "user": { + "id": "11", + "name": "Bob Smith", + "givenName": "Bob", + "familyName": "Smith", + "email": "BobSmith@email.com", + "createdAt": "1901-03-26T20:54:31.7299699Z", + "updatedAt": null + } + } + ] +} \ No newline at end of file diff --git a/client/public/mock/stuff/1.json b/client/public/mock/stuff/1.json new file mode 100644 index 0000000..cd40413 --- /dev/null +++ b/client/public/mock/stuff/1.json @@ -0,0 +1,17 @@ +{ + "id": "1", + "label": "aaa", + "description": "aaa", + "otherInfo": "aaa", + "createdAt": "1901-03-26T19:18:37.781402Z", + "updatedAt": "1901-03-26T21:03:05.3683671Z", + "user": { + "id": "1", + "name": "Alice Smith", + "givenName": "Alice", + "familyName": "Smith", + "email": "AliceSmith@email.com", + "createdAt": "1901-03-26T20:54:31.7299699Z", + "updatedAt": null + } +} \ No newline at end of file diff --git a/client/public/mock/stuff/2.json b/client/public/mock/stuff/2.json new file mode 100644 index 0000000..ff6e3bb --- /dev/null +++ b/client/public/mock/stuff/2.json @@ -0,0 +1,17 @@ +{ + "id": "2", + "label": "bbb", + "description": "bbb", + "otherInfo": "bbb", + "createdAt": "1901-03-26T19:19:43.9789358Z", + "updatedAt": "1901-03-26T21:02:59.8469004Z", + "user": { + "id": "1", + "name": "Alice Smith", + "givenName": "Alice", + "familyName": "Smith", + "email": "AliceSmith@email.com", + "createdAt": "1901-03-26T20:54:31.7299699Z", + "updatedAt": null + } +} \ No newline at end of file diff --git a/client/rollup.config.js b/client/rollup.config.js new file mode 100644 index 0000000..6abe51f --- /dev/null +++ b/client/rollup.config.js @@ -0,0 +1,93 @@ +import svelte from 'rollup-plugin-svelte'; +import commonjs from '@rollup/plugin-commonjs'; +import resolve from '@rollup/plugin-node-resolve'; +import livereload from 'rollup-plugin-livereload'; +import { terser } from 'rollup-plugin-terser'; +import css from 'rollup-plugin-css-only'; + +import replace from '@rollup/plugin-replace'; +import json from 'rollup-plugin-json'; + +import sveltePreprocess from 'svelte-preprocess'; + +const production = !process.env.ROLLUP_WATCH; + +function serve() { + let server; + + function toExit() { + if (server) server.kill(0); + } + + return { + writeBundle() { + if (server) return; + server = require('child_process').spawn('npm', ['run', 'start', '--', '--dev'], { + stdio: ['ignore', 'inherit', 'inherit'], + shell: true + }); + + process.on('SIGTERM', toExit); + process.on('exit', toExit); + } + }; +} + +export default { + input: 'src/main.js', + output: { + sourcemap: !production, + format: 'iife', + name: 'app', + file: 'public/build/bundle.js' + }, + plugins: [ + svelte({ + compilerOptions: { + // enable run-time checks when not in production + dev: !production + }, + preprocess: sveltePreprocess({ + sourceMap: !production, + scss: { + prependData: '@import "./node_modules/bootstrap/scss/bootstrap.scss";' + } + }) + }), + replace({ + preventAssignment: true, + isDev: !production, + isProd: production + }), + // we'll extract any component CSS out into + // a separate file - better for performance + css({ output: 'bundle.css' }), + json(), + + // If you have external dependencies installed from + // npm, you'll most likely need these plugins. In + // some cases you'll need additional configuration - + // consult the documentation for details: + // https://github.com/rollup/plugins/tree/master/packages/commonjs + resolve({ + browser: true, + dedupe: ['svelte'] + }), + commonjs(), + + // In dev mode, call `npm run start` once + // the bundle has been generated + !production && serve(), + + // Watch the `public` directory and refresh the + // browser on changes when not in production + !production && livereload('public'), + + // If we're building for production (npm run build + // instead of npm run dev), minify + production && terser() + ], + watch: { + clearScreen: false + } +}; diff --git a/client/scripts/setupTypeScript.js b/client/scripts/setupTypeScript.js new file mode 100644 index 0000000..133658a --- /dev/null +++ b/client/scripts/setupTypeScript.js @@ -0,0 +1,121 @@ +// @ts-check + +/** This script modifies the project to support TS code in .svelte files like: + + + + As well as validating the code for CI. + */ + +/** To work on this script: + rm -rf test-template template && git clone sveltejs/template test-template && node scripts/setupTypeScript.js test-template +*/ + +const fs = require("fs") +const path = require("path") +const { argv } = require("process") + +const projectRoot = argv[2] || path.join(__dirname, "..") + +// Add deps to pkg.json +const packageJSON = JSON.parse(fs.readFileSync(path.join(projectRoot, "package.json"), "utf8")) +packageJSON.devDependencies = Object.assign(packageJSON.devDependencies, { + "svelte-check": "^2.0.0", + "svelte-preprocess": "^4.0.0", + "@rollup/plugin-typescript": "^8.0.0", + "typescript": "^4.0.0", + "tslib": "^2.0.0", + "@tsconfig/svelte": "^2.0.0" +}) + +// Add script for checking +packageJSON.scripts = Object.assign(packageJSON.scripts, { + "check": "svelte-check --tsconfig ./tsconfig.json" +}) + +// Write the package JSON +fs.writeFileSync(path.join(projectRoot, "package.json"), JSON.stringify(packageJSON, null, " ")) + +// mv src/main.js to main.ts - note, we need to edit rollup.config.js for this too +const beforeMainJSPath = path.join(projectRoot, "src", "main.js") +const afterMainTSPath = path.join(projectRoot, "src", "main.ts") +fs.renameSync(beforeMainJSPath, afterMainTSPath) + +// Switch the app.svelte file to use TS +const appSveltePath = path.join(projectRoot, "src", "App.svelte") +let appFile = fs.readFileSync(appSveltePath, "utf8") +appFile = appFile.replace(" + +
+
+ +
+ {#if $authError} + + {/if} + {#if $isAuthenticated} + + + + + + + + + + + + + + + + + {/if} + + {#if displayOidcConf} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
storevalue
isLoading{$isLoading}
isAuthenticated{$isAuthenticated}
accessToken + {$accessToken} +
idToken{$idToken}
userInfo +
{JSON.stringify($userInfo, null, 2) || ""}
+
authError{$authError}
+ {/if} + +
+
+ + diff --git a/client/src/api/stuff.js b/client/src/api/stuff.js new file mode 100644 index 0000000..5a21e0b --- /dev/null +++ b/client/src/api/stuff.js @@ -0,0 +1,138 @@ +// stuffList.js +import axios from "axios"; + +const rootApi = isProd ? "https://localhost:5001/api/stuff" : "http://localhost:3000/mock/stuff"; +const isMock = rootApi.indexOf("mock") > -1; + +export const apiGetStuffList = async (accessToken, idToken) => { + const mock = isMock ? ".json" : ""; + const getMsg = { + method: "get", + headers: { "authorization": `Bearer ${accessToken}`, "id_token": idToken }, + url: rootApi + mock, + data: {} + }; + + try { + const result = await axios(getMsg); + return result.data; + } catch (error) { + return { error: getErrorMsg(error) }; + } +}; + +export const apiSearchStuff = async (search, accessToken, idToken) => { + const mock = isMock ? ".json" : ""; + const getMsg = { + method: "get", + headers: { "authorization": `Bearer ${accessToken}`, "id_token": idToken }, + url: `${rootApi}${mock}?search=${search}`, + data: {} + }; + + try { + const result = await axios(getMsg); + return result.data; + } catch (error) { + return { error: getErrorMsg(error) }; + } +}; + +export const apiGotoPage = async (page, accessToken, idToken) => { + const mock = isMock ? ".json" : ""; + const getMsg = { + method: "get", + headers: { "authorization": `Bearer ${accessToken}`, "id_token": idToken }, + url: `${rootApi}${mock}?page=${page}`, + data: {} + }; + + try { + const result = await axios(getMsg); + return result.data; + } catch (error) { + return { error: getErrorMsg(error) }; + } +}; + +export const apiGetStuffById = async (id, accessToken, idToken) => { + const mock = isMock ? ".json" : ""; + const getMsg = { + method: "get", + headers: { "authorization": `Bearer ${accessToken}`, "id_token": idToken }, + url: `${rootApi}/${id}${mock}`, + data: {} + }; + + try { + const result = await axios(getMsg); + return result.data; + } catch (error) { + return { error: getErrorMsg(error) }; + } +}; + +export const apiCreateStuff = async (input, accessToken, idToken) => { + const mock = isMock ? ".json" : ""; + const postMsg = { + method: isMock ? "get" : "post", + headers: { "authorization": `Bearer ${accessToken}`, "id_token": idToken }, + url: rootApi + mock, + data: input + }; + + try { + const result = await axios(postMsg); + return result.data; + } catch (error) { + return { error: getErrorMsg(error) }; + } +}; + +export const apiUpdateStuff = async (id, input, accessToken, idToken) => { + const mock = isMock ? ".json" : ""; + const putMsg = { + method: isMock ? "get" : "put", + headers: { "authorization": `Bearer ${accessToken}`, "id_token": idToken }, + url: `${rootApi}/${id}${mock}`, + data: input + }; + + try { + const result = await axios(putMsg); + return result.data; + } catch (error) { + return { error: getErrorMsg(error) }; + } +}; + +export const apiDeleteStuff = async (id, accessToken, idToken) => { + const mock = isMock ? ".json" : ""; + const deleteMsg = { + method: isMock ? "get" : "delete", + headers: { "authorization": `Bearer ${accessToken}`, "id_token": idToken }, + url: `${rootApi}/${id}${mock}`, + data: {} + }; + + try { + const result = await axios(deleteMsg); + return result.data; + } catch (error) { + return { error: getErrorMsg(error) }; + } +}; + +const getErrorMsg = error => { + const msg = "An error occured. Try again later."; + if (error.response && error.response.data && error.response.data.error) { + return error.response.data.error; + } + if (error.message) { + return error.message; + } + if (error) { + return error; + } + return msg; +}; \ No newline at end of file diff --git a/client/src/components/CommonForm.svelte b/client/src/components/CommonForm.svelte new file mode 100644 index 0000000..dd2dc2f --- /dev/null +++ b/client/src/components/CommonForm.svelte @@ -0,0 +1,121 @@ + + +
+ {#if !stuffDatum.id} + {#if stuffDatum.error} + + {:else} + + {/if} + {/if} + + {#if inputError} + + {/if} + + {#if stuffDatum.id} +
+ +