diff --git a/.g8/form/app/controllers/$model__Camel$Controller.scala b/.g8/form/app/controllers/$model__Camel$Controller.scala new file mode 100644 index 000000000..aff914c84 --- /dev/null +++ b/.g8/form/app/controllers/$model__Camel$Controller.scala @@ -0,0 +1,46 @@ +package controllers + +import javax.inject._ +import play.api.mvc._ + +import play.api.data._ +import play.api.data.Forms._ + +case class $model;format="Camel"$Data(name: String, age: Int) + +// NOTE: Add the following to conf/routes to enable compilation of this class: +/* +GET /$model;format="camel"$ controllers.$model;format="Camel"$Controller.$model;format="camel"$Get +POST /$model;format="camel"$ controllers.$model;format="Camel"$Controller.$model;format="camel"$Post +*/ + +/** + * $model;format="Camel"$ form controller for Play Scala + */ +class $model;format="Camel"$Controller @Inject()(mcc: MessagesControllerComponents) extends MessagesAbstractController(mcc) { + + val $model;format="camel"$Form = Form( + mapping( + "name" -> text, + "age" -> number + )($model;format="Camel"$Data.apply)($model;format="Camel"$Data.unapply) + ) + + def $model;format="camel"$Get() = Action { implicit request: MessagesRequest[AnyContent] => + Ok(views.html.$model;format="camel"$.form($model;format="camel"$Form)) + } + + def $model;format="camel"$Post() = Action { implicit request: MessagesRequest[AnyContent] => + $model;format="camel"$Form.bindFromRequest.fold( + formWithErrors => { + // binding failure, you retrieve the form containing errors: + BadRequest(views.html.$model;format="camel"$.form(formWithErrors)) + }, + $model;format="camel"$Data => { + /* binding success, you get the actual value. */ + /* flashing uses a short lived cookie */ + Redirect(routes.$model;format="Camel"$Controller.$model;format="camel"$Get()).flashing("success" -> ("Successful " + $model;format="camel"$Data.toString)) + } + ) + } +} diff --git a/.g8/form/app/views/$model__camel$/form.scala.html b/.g8/form/app/views/$model__camel$/form.scala.html new file mode 100644 index 000000000..14674ba6e --- /dev/null +++ b/.g8/form/app/views/$model__camel$/form.scala.html @@ -0,0 +1,12 @@ +@($model;format="camel"$Form: Form[$model;format="Camel"$Data])(implicit request: MessagesRequestHeader) + +

$model;format="camel"$ form

+ +@request.flash.get("success").getOrElse("") + +@helper.form(action = routes.$model;format="Camel"$Controller.$model;format="camel"$Post()) { + @helper.CSRF.formField + @helper.inputText($model;format="camel"$Form("name")) + @helper.inputText($model;format="camel"$Form("age")) + +} diff --git a/.g8/form/default.properties b/.g8/form/default.properties new file mode 100644 index 000000000..32090f30c --- /dev/null +++ b/.g8/form/default.properties @@ -0,0 +1,2 @@ +description = Generates a Controller with form handling +model = user diff --git a/.g8/form/test/controllers/$model__Camel$ControllerSpec.scala b/.g8/form/test/controllers/$model__Camel$ControllerSpec.scala new file mode 100644 index 000000000..d25174315 --- /dev/null +++ b/.g8/form/test/controllers/$model__Camel$ControllerSpec.scala @@ -0,0 +1,71 @@ +package controllers + +import play.api.mvc._ +import play.api.i18n._ +import org.scalatestplus.play._ +import org.scalatestplus.play.guice.GuiceOneAppPerTest +import play.api.http.FileMimeTypes +import play.api.test._ +import play.api.test.Helpers._ +import play.api.test.CSRFTokenHelper._ + +import scala.concurrent.ExecutionContext + +/** + * $model;format="Camel"$ form controller specs + */ +class $model;format="Camel"$ControllerSpec extends PlaySpec with GuiceOneAppPerTest with Injecting { + + // Provide stubs for components based off Helpers.stubControllerComponents() + class StubComponents(cc:ControllerComponents = stubControllerComponents()) extends MessagesControllerComponents { + override val parsers: PlayBodyParsers = cc.parsers + override val messagesApi: MessagesApi = cc.messagesApi + override val langs: Langs = cc.langs + override val fileMimeTypes: FileMimeTypes = cc.fileMimeTypes + override val executionContext: ExecutionContext = cc.executionContext + override val actionBuilder: ActionBuilder[Request, AnyContent] = cc.actionBuilder + override val messagesActionBuilder: MessagesActionBuilder = new DefaultMessagesActionBuilderImpl(parsers.default, messagesApi)(executionContext) + } + + "$model;format="Camel"$Controller GET" should { + + "render the index page from a new instance of controller" in { + val controller = new $model;format="Camel"$Controller(new StubComponents()) + val request = FakeRequest().withCSRFToken + val home = controller.$model;format="camel"$Get().apply(request) + + status(home) mustBe OK + contentType(home) mustBe Some("text/html") + } + + "render the index page from the application" in { + val controller = inject[$model;format="Camel"$Controller] + val request = FakeRequest().withCSRFToken + val home = controller.$model;format="camel"$Get().apply(request) + + status(home) mustBe OK + contentType(home) mustBe Some("text/html") + } + + "render the index page from the router" in { + val request = CSRFTokenHelper.addCSRFToken(FakeRequest(GET, "/$model;format="camel"$")) + val home = route(app, request).get + + status(home) mustBe OK + contentType(home) mustBe Some("text/html") + } + } + + "$model;format="Camel"$Controller POST" should { + "process form" in { + val request = { + FakeRequest(POST, "/$model;format="camel"$") + .withFormUrlEncodedBody("name" -> "play", "age" -> "4") + } + val home = route(app, request).get + + status(home) mustBe SEE_OTHER + } + } + +} diff --git a/.github/workflows/doc.yml b/.github/workflows/doc.yml new file mode 100644 index 000000000..8b39d7451 --- /dev/null +++ b/.github/workflows/doc.yml @@ -0,0 +1,40 @@ +name: Build dev documentations + +on: + push: + paths: + - 'manual/**' + - '.github/workflows/doc.yml' + branches: + - main + workflow_dispatch: + + +jobs: + build_dev_manual: + name: Build developer documentation + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-node@v3 + with: + node-version: '18.x' + - name: Generate documentation website + id: doc + run: | + cd manual + npm ci + npm run build + rm -rf ../doc + mv ./build ../doc + - name: Commit files + run: | + git config --local user.email "izanami-release-bot@users.noreply.github.com" + git config --local user.name "izanami release bot" + git add --all + git commit -am "Update dev documentation" + - name: Push documentation + uses: ad-m/github-push-action@master + with: + branch: main + github_token: ${{ secrets.PERSONAL_ACCESS_TOKEN}} \ No newline at end of file diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml new file mode 100644 index 000000000..b275d132b --- /dev/null +++ b/.github/workflows/main.yml @@ -0,0 +1,38 @@ +name: Playwright + +on: + push: + branches: [ "master" ] + pull_request: + branches: [ "master" ] + +permissions: + contents: read + + +jobs: + tests_e2e: + name: Run end-to-end tests + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-node@v3 + - name: Install dependencies + run: npm ci + working-directory: ./izanami-frontend + - name: Install playwright browsers + run: npx playwright install --with-deps chromium + working-directory: ./izanami-frontend + - name: Set up JDK 21 + uses: actions/setup-java@v3 + with: + java-version: '21' + distribution: 'temurin' + cache: 'sbt' + - name: Start docker + run: docker-compose up -d + - name: start backend + run: sbt "bgRun -Dconfig.resource=dev.conf" + - name: Run tests + run: npx playwright test --project chromium + working-directory: ./izanami-frontend diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 000000000..bf113a671 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,53 @@ +name: release + +on: + workflow_dispatch: + inputs: + releaseVersion: + description: 'release version' + required: true + +jobs: + release: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2.2.0 + - name: Set up JDK 11 + uses: actions/setup-java@v3 + with: + java-version: '21' + distribution: 'temurin' + - uses: coursier/cache-action@v5 + # install node lts + - name: setup node + uses: actions/setup-node@v3 + with: + node-version: '20.x' + cache: 'npm' + cache-dependency-path: izanami-frontend/package-lock.json + - name: build frontend + run: | + cd izanami-frontend + npm ci + npm run build + - name: build backend + run: sbt "set test in assembly := {}" clean assembly + - name: release + run: | + git checkout . + git config --global user.email "izanami@users.noreply.github.com" + git config --global user.name "izanami release bot" + sbt "release release-version ${{ github.event.inputs.releaseVersion }} with-defaults skip-tests" + ls target + ls target/scala-2.13 + - name: github release + uses: softprops/action-gh-release@v2 + with: + tag_name: v${{ github.event.inputs.releaseVersion }} + files: | + target/scala-2.13/izanami.jar + - name: next version + run: | + git add . + git commit -am "Next dev version" + git push \ No newline at end of file diff --git a/.github/workflows/scala.yml b/.github/workflows/scala.yml new file mode 100644 index 000000000..cfbf087eb --- /dev/null +++ b/.github/workflows/scala.yml @@ -0,0 +1,38 @@ +# This workflow uses actions that are not certified by GitHub. +# They are provided by a third-party and are governed by +# separate terms of service, privacy policy, and support +# documentation. + +name: Scala CI + +on: + push: + branches: [ "master" ] + pull_request: + branches: [ "master" ] + +permissions: + contents: read + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - name: Set up JDK 21 + uses: actions/setup-java@v3 + with: + java-version: '21' + distribution: 'temurin' + cache: 'sbt' + #- name: Start docker + # run: docker-compose up -d + #- name: debug wasmo + # run: curl -L 127.0.0.1:5001/api/plugins + - name: Run tests + run: sbt test + #- name: Stop docker + # run: docker-compose down + # Optional: This step uploads information to the GitHub dependency graph and unblocking Dependabot alerts for the repository + #- name: Upload dependency graph + # uses: scalacenter/sbt-dependency-submission@ab086b50c947c9774b70f39fc7f6e20ca2706c91 diff --git a/.gitignore b/.gitignore new file mode 100644 index 000000000..3a33bd8b1 --- /dev/null +++ b/.gitignore @@ -0,0 +1,24 @@ +izanami.jar +.secrets +logs +target +/.bsp +/.idea +/.idea_modules +/.classpath +/.project +/.settings +/RUNNING_PID +node_modules/ +javascript/.parcel-cache/ +.DS_Store +test/Sandbox.scala +/public/assets/ +/public/index.html +.bloop/ +.metals/ +.vscode/ +project/.bloop/ +project/metals.sbt +project/project/ +izanami-frontend/playwright/.auth/ diff --git a/.java-version b/.java-version new file mode 100644 index 000000000..aabe6ec39 --- /dev/null +++ b/.java-version @@ -0,0 +1 @@ +21 diff --git a/.scalafmt.conf b/.scalafmt.conf new file mode 100644 index 000000000..1a5424aac --- /dev/null +++ b/.scalafmt.conf @@ -0,0 +1,11 @@ +version = 2.7.5 + +style = defaultWithAlign + +align = most +danglingParentheses = true +docstrings = JavaDoc +indentOperator = spray +maxColumn = 120 +rewrite.rules = [RedundantParens, SortImports] +unindentTopLevelOperators = true \ No newline at end of file diff --git a/DockerFile b/DockerFile new file mode 100644 index 000000000..bbbde4c05 --- /dev/null +++ b/DockerFile @@ -0,0 +1,9 @@ +FROM eclipse-temurin:21 +RUN mkdir /app +RUN groupadd -g 10001 javauser && useradd -u 10000 -g javauser javauser +ENV IZANAMI_CONTAINERIZED=true +COPY ./target/scala-2.13/izanami.jar /app/izanami.jar +WORKDIR /app +RUN chown -R javauser:javauser /app +USER javauser +CMD "java" "-jar" "izanami.jar" \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 000000000..af2e77cc9 --- /dev/null +++ b/README.md @@ -0,0 +1,90 @@ +# Izanami + +image + +This README is for anyone who would like to contribute to Izanami. + +If you're interested in Izanami documentation, [it's here](). + +## Start application locally + +### Izanami frontend + +```sh +cd izanami-frontend +npm run dev +``` + +### Local database & misc tools + +```sh +docker-compose rm -f && docker-compose up +``` + +### Izanami backend + +```sh +sbt -jvm-debug 5005 +~run -Dconfig.resource=dev.conf +``` + +Once everything is started, just browse to [localhost:3000](http://localhost:3000). + + +In a developement setup, it may be usefull to craft tokens with longer TTL + +``` +sbt -jvm-debug 5005 +~run -Dconfig.resource=dev.conf -Dapp.sessions.TTL=604800 +``` + +### Backend tests + +To run test, you can either start Izanami and associated tooling (db, ...) with above commands or just run a suite / a test. + +In fact, an Izanami instance and docker containers will be started by tests if none is running. This could be usefull for coverage / debug. + +You'll need docker-compose installed locally to run tests like this, due to [this issue](https://github.com/testcontainers/testcontainers-java/issues/7239). + +#### Colima setup + +To run test without having starting docker-compose, you'll need these env variables to be set. + +```sh +DOCKER_HOST=unix://${HOME}/.colima/default/docker.sock; +RYUK_CONTAINER_PRIVILEGED=true; +TESTCONTAINERS_DOCKER_SOCKET_OVERRIDE=/var/run/docker.sock +``` + + +## Package application + +To package frontend : + +```sh +cd izanami-frontend +npm run build +``` + +To package backend (make sure to package frontend first) : + +```sh +sbt "set test in assembly := {}" clean assembly +``` + +To start generated jar + +```sh +java -Dconfig.resource=dev.conf -jar ./target/scala-2.13/izanami.jar +``` + +To build docker image (after packaging frontend and backends) + +```sh +docker build -t izanami . +``` +To test docker image + +```sh +docker run --env IZANAMI_PG_URI=postgresql://postgres:postgres@host.docker.internal:5432/postgres -p 9000:9000 izanami +``` diff --git a/app/fr/maif/izanami/application.scala b/app/fr/maif/izanami/application.scala new file mode 100644 index 000000000..2c239946a --- /dev/null +++ b/app/fr/maif/izanami/application.scala @@ -0,0 +1,115 @@ +package fr.maif.izanami + +import com.softwaremill.macwire.wire +import controllers.{Assets, AssetsComponents} +import fr.maif.izanami.env.Env +import fr.maif.izanami.errors.IzanamiHttpErrorHandler +import fr.maif.izanami.v1.WasmManagerClient +import fr.maif.izanami.web.{ClientApiKeyAction, _} +import play.api.ApplicationLoader.Context +import play.api._ +import play.api.http.{DefaultHttpFilters, HttpErrorHandler} +import play.api.libs.ws.ahc.AhcWSComponents +import play.api.mvc.EssentialFilter +import play.api.routing.Router +import play.filters.HttpFiltersComponents +import play.filters.cors.{CORSConfig, CORSFilter} +import play.filters.csp.{CSPComponents, CSPFilter, CSPResultProcessor} +import play.filters.csrf.CSRFFilter +import play.filters.gzip.GzipFilterComponents +import play.filters.headers.{SecurityHeadersConfig, SecurityHeadersFilter} +import play.filters.https.{RedirectHttpsComponents, RedirectHttpsConfiguration, RedirectHttpsFilter} +import router.Routes + +import scala.concurrent.duration.DurationInt +import scala.concurrent.{Await, Future} + +class IzanamiLoader extends ApplicationLoader { + private val logger = Logger("IzanamiLoader") + + def load(context: ApplicationLoader.Context): Application = { + LoggerConfigurator(context.environment.classLoader).foreach { + _.configure(context.environment, context.initialConfiguration, Map.empty) + } + val components = new IzanamiComponentsInstances(context) + Await.result(components.onStart(), 10.seconds) + components.application + } +} + +class IzanamiComponentsInstances( + context: Context +) extends BuiltInComponentsFromContext(context) + with AssetsComponents + with AhcWSComponents + with HttpFiltersComponents + with CSPComponents + with RedirectHttpsComponents + with GzipFilterComponents { + + override lazy val httpFilters: Seq[EssentialFilter] = super.httpFilters.filter { + case _: CSRFFilter => false + case _ => false + } :+ corsFilter :+ /*cspFilter :+ redirectHttpsFilter :*/ gzipFilter + override lazy val httpErrorHandler: HttpErrorHandler = wire[IzanamiHttpErrorHandler] + + implicit lazy val env: Env = new Env( + configuration = configuration, + environment = environment, + Ws = wsClient + ) + + lazy val filters = new DefaultHttpFilters(httpFilters: _*) + + lazy val authAction = wire[TenantAuthActionFactory] + lazy val tenantAuthAction = wire[ProjectAuthActionFactory] + lazy val adminAuthAction = wire[AdminAuthAction] + lazy val keyAuthAction = wire[KeyAuthActionFactory] + lazy val authenticatedAction = wire[AuthenticatedAction] + lazy val detailledAuthAction = wire[DetailledAuthAction] + lazy val detailledRightForTenantFactory = wire[DetailledRightForTenantFactory] + lazy val tenantRightsAction = wire[TenantRightsAction] + lazy val sessionAuthAction = wire[AuthenticatedSessionAction] + lazy val wasmManagerClient = wire[WasmManagerClient] + lazy val clientApiKeyAction = wire[ClientApiKeyAction] + + lazy val featureController = wire[FeatureController] + lazy val tenantController = wire[TenantController] + lazy val projectController = wire[ProjectController] + lazy val tagController = wire[TagController] + lazy val apiKeyController = wire[ApiKeyController] + lazy val featureContextController = wire[FeatureContextController] + lazy val userController = wire[UserController] + lazy val loginController = wire[LoginController] + lazy val configurationController = wire[ConfigurationController] + lazy val pluginController = wire[PluginController] + lazy val importController = wire[ImportController] + lazy val legacyController = wire[LegacyController] + lazy val eventController = wire[EventController] + lazy val frontendController = wire[FrontendController] + + override lazy val assets: Assets = wire[Assets] + lazy val router: Router = { + // add the prefix string in local scope for the Routes constructor + val prefix: String = "/" + wire[Routes] + } + + def onStart(): Future[Unit] = { + applicationLifecycle.addStopHook { () => + env.onStop() + } + env + .onStart() + } + + def corsFilter: CORSFilter = { + new CORSFilter(CORSConfig.fromConfiguration(env.configuration)) + } + + /*def redirectHttpsFilter: RedirectHttpsFilter = { + RedirectHttpsConfigurationProvider + new RedirectHttpsFilter(RedirectHttpsConfiguration.(env.configuration)) + }*/ + +} diff --git a/app/fr/maif/izanami/datastores/ApiKeyDatastore.scala b/app/fr/maif/izanami/datastores/ApiKeyDatastore.scala new file mode 100644 index 000000000..4f94236d8 --- /dev/null +++ b/app/fr/maif/izanami/datastores/ApiKeyDatastore.scala @@ -0,0 +1,387 @@ +package fr.maif.izanami.datastores + +import fr.maif.izanami.datastores.apiKeyImplicites.ApiKeyRow +import fr.maif.izanami.env.Env +import fr.maif.izanami.env.PostgresqlErrors.{FOREIGN_KEY_VIOLATION, RELATION_DOES_NOT_EXISTS} +import fr.maif.izanami.env.pgimplicits.EnhancedRow +import fr.maif.izanami.errors._ +import fr.maif.izanami.models.{ApiKey, ApiKeyProject, ApiKeyWithCompleteRights} +import fr.maif.izanami.utils.Datastore +import fr.maif.izanami.utils.syntax.implicits.BetterSyntax +import fr.maif.izanami.web.ImportController.{Fail, ImportConflictStrategy} +import io.vertx.pgclient.PgException +import io.vertx.sqlclient.{Row, SqlConnection} + +import java.util.UUID +import scala.List +import scala.concurrent.Future + +class ApiKeyDatastore(val env: Env) extends Datastore { + def createApiKey( + apiKey: ApiKey, + user: String + ): Future[Either[IzanamiError, ApiKey]] = { + createApiKeys(apiKey.tenant, apiKeys = Seq(apiKey), user=user, conflictStrategy = Fail, conn= None) + .map(e => e.map(_.head).left.map(_.head)) + } + + def findLegacyKeyTenant(clientId: String): Future[Option[String]] = { + env.postgresql.queryOne( + s"""SELECT tenant FROM izanami.key_tenant WHERE client_id = $$1""", + List(clientId) + ){r => r.optString("tenant")} + } + + def createApiKeys( + tenant: String, + apiKeys: Seq[ApiKey], + user: String, + conflictStrategy: ImportConflictStrategy, + conn: Option[SqlConnection] + ): Future[Either[Seq[IzanamiError], Seq[ApiKey]]] = { + // TODO handle conflict strategy + + def callback(connection: SqlConnection): Future[Either[Seq[IzanamiError], Seq[ApiKey]]] = { + env.postgresql + .queryAll( + s"""insert into apikeys (name, clientid, clientsecret, description, enabled, legacy, admin) + |values (unnest($$1::text[]), unnest($$2::text[]), unnest($$3::text[]), unnest($$4::text[]), unnest($$5::boolean[]), unnest($$6::boolean[]), unnest($$7::boolean[])) returning *""".stripMargin, + List( + apiKeys.map(_.name).toArray, + apiKeys.map(_.clientId).toArray, + apiKeys.map(key => key.clientSecret).toArray, + apiKeys.map(_.description).toArray, + apiKeys.map(k => java.lang.Boolean.valueOf(k.enabled)).toArray, + apiKeys.map(k => java.lang.Boolean.valueOf(k.legacy)).toArray, + apiKeys.map(k => java.lang.Boolean.valueOf(k.admin)).toArray + ), + conn = Some(connection), + schemas=Set(tenant) + ) { row => { + val requestKey = apiKeys.find(k => k.name == row.getString("name")).get + row.optApiKey(requestKey.tenant).map(key => key.copy(clientSecret = requestKey.clientSecret)) + } + } + .flatMap(_ => { + val futures: Seq[Future[Either[IzanamiError, ApiKey]]] = apiKeys.filter(key => key.projects.nonEmpty) + .map(apiKey => + env.postgresql + .queryOne( + s""" + |INSERT INTO apikeys_projects (apikey, project) + |SELECT $$1, unnest($$2::TEXT[]) + |RETURNING * + |""".stripMargin, + List(apiKey.name, apiKey.projects.toArray), + conn = Some(connection), + schemas=Set(tenant) + ) { _ => Some(apiKey) } + .map { + _.toRight(InternalServerError()) + } + .recover { + case f: PgException if f.getSqlState == FOREIGN_KEY_VIOLATION => + Left(OneProjectDoesNotExists(apiKey.projects)) + case ex => + logger.error("Failed to update project mapping table", ex) + Left(InternalServerError()) + } + ) + Future.sequence(futures) + }) + .flatMap(eitherKey => { + val errors = eitherKey.toList.filter(_.isLeft) + .map(_.swap.toOption).flatMap(_.toList) + errors match { + case Nil => { + env.postgresql + .queryAll( + s""" + |INSERT INTO users_keys_rights(username, apikey, level) + |VALUES (unnest($$1::text[]), unnest($$2::text[]), 'ADMIN') + |RETURNING apikey + |""".stripMargin, + List(Array.fill(apiKeys.size)(user), apiKeys.map(_.name).toArray), + conn = Some(connection), + schemas = Set(tenant) + ) { r => apiKeys.find(k => k.name == r.getString("apikey")) } + .map(l => Right(l)) + } + case _ => Left(errors).future + } + }).flatMap(either => { + // FIXME remove this filter to add a legacy flag + val clientIds = either.getOrElse(List()).map(_.clientId).filter(clientId => !clientId.contains("_")) + if(clientIds.isEmpty) { + either.future + } else { + env.postgresql.queryOne( + s""" + |INSERT INTO izanami.key_tenant (client_id, tenant) VALUES (unnest($$1::TEXT[]), $$2) + |RETURNING tenant + |""".stripMargin, + List(clientIds.toArray, tenant), + conn=Some(connection) + ){_ => Some(())} + .map(o => o.toRight(Seq(InternalServerError()))) + .map(e => e.flatMap(_ => either)) + } + }) + } + + if (apiKeys.isEmpty) { + Future.successful(Right(Seq())) + } else { + conn.map(c => callback(c)).getOrElse(env.postgresql.executeInTransaction(c => callback(c))) + } + } + + def readApiKeys(tenant: String, username: String): Future[List[ApiKey]] = { + env.postgresql.queryAll( + s""" + |SELECT + |a.clientid, + |a.name, + |a.description, + |a.enabled, + |a.legacy, + |a.admin, + |a.clientsecret, + |COALESCE(json_agg(ap.project) FILTER (WHERE ap.project IS NOT NULL), '[]') AS projects + |FROM apikeys a + |LEFT JOIN apikeys_projects ap + |ON ap.apikey = a.name + |WHERE EXISTS (SELECT u.username FROM izanami.users u WHERE u.username=$$1 AND u.admin=TRUE) + |OR EXISTS(SELECT * FROM izanami.users_tenants_rights utr WHERE utr.username=$$1 AND (utr.level='ADMIN')) + |OR a.name=ANY(SELECT akr.apikey FROM users_keys_rights akr WHERE akr.username=$$1) + |GROUP BY a.name + |""".stripMargin, + List(username), + schemas = Set(tenant) + ) { r => r.optApiKeyWithSubObjects(tenant) } + } + + def readApiKey(tenant: String, name: String): Future[Option[ApiKey]] = { + env.postgresql.queryOne( + s""" + |SELECT + |a.clientid, + |a.name, + |a.description, + |a.enabled, + |a.legacy, + |a.admin, + |a.clientsecret, + |COALESCE(json_agg(ap.project) FILTER (WHERE ap.project IS NOT NULL), '[]') AS projects + |FROM apikeys a + |LEFT JOIN apikeys_projects ap + |ON ap.apikey = a.name + |WHERE a.name = $$1 + |GROUP BY a.name + |""".stripMargin, + List(name), + schemas = Set(tenant) + ) { r => r.optApiKeyWithSubObjects(tenant) } + } + + def deleteApiKey(tenant: String, name: String): Future[Either[IzanamiError, String]] = { + env.postgresql.executeInTransaction(conn => { + env.postgresql + .queryOne( + s""" + DELETE FROM apikeys WHERE name=$$1 RETURNING clientid + |""".stripMargin, + List(name), + schemas = Set(tenant), + conn=Some(conn) + ) { row => row.optString("clientid") } + .map(o => o.toRight(KeyNotFound(name))) + .flatMap { + case Left(value) => Left(value).future + case Right(clientId) => + env.postgresql.queryRaw( + s"DELETE FROM izanami.key_tenant WHERE client_id=$$1", + List(clientId), + conn=Some(conn) + ){_ => Right(clientId)} + } + }) + } + + def updateApiKey(tenant: String, oldName: String, newKey: ApiKey): Future[Either[IzanamiError, Unit]] = { + env.postgresql.executeInTransaction( + conn => { + env.postgresql + .queryRaw( + s""" + |DELETE FROM apikeys_projects WHERE apikey = $$1 + |""".stripMargin, + List(oldName), + conn = Some(conn) + ) { _ => Right(()) } + .flatMap(_ => { + env.postgresql + .queryOne( + s""" + UPDATE apikeys + |SET name=$$1, + |description=$$2, + |enabled=$$4, + |admin=$$5 + |WHERE name=$$3 + |RETURNING name + |""".stripMargin, + List(newKey.name, newKey.description, oldName, java.lang.Boolean.valueOf(newKey.enabled), java.lang.Boolean.valueOf(newKey.admin)), + conn = Some(conn) + ) { row => row.optString("name") } + .map(o => o.toRight(KeyNotFound(oldName))) + }) + .flatMap(_ => { + if (newKey.projects.nonEmpty) { + env.postgresql.queryRaw( + s""" + |INSERT INTO apikeys_projects (apikey, project) + |SELECT $$1, unnest($$2::text[]) + |""".stripMargin, + List(newKey.name, newKey.projects.toArray), + conn = Some(conn) + ) { _ => Right(()) } + } else { + Future.successful(Right(())) + } + }) + }, + schemas = Set(tenant) + ) + } + + def readAndCheckApiKey( + clientId: String, + clientSecret: String + ): Future[Either[IzanamiError, ApiKeyWithCompleteRights]] = { + val futureMaybeTenant = ApiKey.extractTenant(clientId) match { + case s@Some(tenant) => Future.successful(s) + case None => findLegacyKeyTenant(clientId) + } + + futureMaybeTenant.flatMap { + case None => Future.successful(Left(ApiKeyDoesNotExist(clientId))) + case Some(tenant) => { + env.postgresql + .queryOne( + s""" + |SELECT + |a.clientid, + |a.clientsecret, + |a.name, + |a.description, + |a.enabled, + |a.legacy, + |a.admin, + |COALESCE(json_agg(json_build_object('id', p.id, 'name', p.name)) FILTER (WHERE p.id IS NOT NULL), '[]') AS projects + |FROM apikeys a + |LEFT JOIN apikeys_projects ap ON ap.apikey = a.name + |LEFT JOIN projects p ON p.name=ap.project + |WHERE a.clientid=$$1 + |AND a.enabled=true + |GROUP BY a.name + |""".stripMargin, + List(clientId), + schemas = Set(tenant) + ) { r => { + val projects = r + .optJsArray("projects") + .map(arr => + arr.value + .map(v => { + for ( + name <- (v \ "name").asOpt[String]; + id <- (v \ "id").asOpt[UUID] + ) yield ApiKeyProject(name, id) + }) + .toSet + ) + .getOrElse(Set()) + .filter(_.isDefined) + .map(_.get) + for ( + _ <- r.optBoolean("enabled").filter(_ == true); + clientSecret <- r.optString("clientsecret"); + clientid <- r.optString("clientid"); + enabled <- r.optBoolean("enabled"); + name <- r.optString("name"); + legacy <- r.optBoolean("legacy"); + admin <- r.optBoolean("admin") + ) + yield ApiKeyWithCompleteRights( + clientId = clientid, + clientSecret=clientSecret, + tenant = tenant, + name = name, + projects = projects, + enabled = enabled, + legacy=legacy, + admin=admin + ) + } + } + .map(o => o.toRight(KeyNotFound(clientId))) + .recover { + case f: PgException if f.getSqlState == RELATION_DOES_NOT_EXISTS => Left(TenantDoesNotExists(tenant)) + case _ => Left(InternalServerError()) + } + } + } + } +} + +object apiKeyImplicites { + implicit class ApiKeyRow(val row: Row) extends AnyVal { + def optApiKey(tenant: String): Option[ApiKey] = { + for ( + clientid <- row.optString("clientid"); + clientsecret <- row.optString("clientsecret"); + name <- row.optString("name"); + description <- row.optString("description"); + enabled <- row.optBoolean("enabled"); + legacy <- row.optBoolean("legacy"); + admin <- row.optBoolean("admin") + ) + yield ApiKey( + clientId = clientid, + clientSecret = clientsecret, + tenant = tenant, + name = name, + description = description, + enabled = enabled, + legacy = legacy, + admin = admin + ) + } + + def optApiKeyWithSubObjects(tenant: String): Option[ApiKey] = { + val projects = row.optJsArray("projects").map(arr => arr.value.map(v => v.as[String]).toSet).getOrElse(Set()) + + for ( + clientid <- row.optString("clientid"); + name <- row.optString("name"); + description <- row.optString("description"); + enabled <- row.optBoolean("enabled"); + legacy <- row.optBoolean("legacy"); + admin <- row.optBoolean("admin"); + clientSecret <- row.optString("clientsecret") + ) + yield ApiKey( + clientId = clientid, + tenant = tenant, + name = name, + projects = projects, + description = description, + enabled = enabled, + legacy = legacy, + admin=admin, + clientSecret=clientSecret + ) + } + } +} diff --git a/app/fr/maif/izanami/datastores/ConfigurationDatastore.scala b/app/fr/maif/izanami/datastores/ConfigurationDatastore.scala new file mode 100644 index 000000000..2684a19aa --- /dev/null +++ b/app/fr/maif/izanami/datastores/ConfigurationDatastore.scala @@ -0,0 +1,248 @@ +package fr.maif.izanami.datastores + +import fr.maif.izanami.datastores.ConfigurationDatastore.{parseDbMailer, parseInvitationMode} +import fr.maif.izanami.datastores.configurationImplicits.{ConfigurationRow, MailerConfigurationRow} +import fr.maif.izanami.env.Env +import fr.maif.izanami.env.pgimplicits.EnhancedRow +import fr.maif.izanami.errors.{ConfigurationReadError, InternalServerError, IzanamiError} +import fr.maif.izanami.mail.MailerTypes.MailerType +import fr.maif.izanami.mail._ +import fr.maif.izanami.models.InvitationMode.InvitationMode +import fr.maif.izanami.models.IzanamiConfiguration.{SMTPConfigurationReads, SMTPConfigurationWrites, mailGunConfigurationReads, mailJetConfigurationReads} +import fr.maif.izanami.models.{FullIzanamiConfiguration, InvitationMode, IzanamiConfiguration, OIDCConfiguration} +import fr.maif.izanami.utils.Datastore +import io.otoroshi.wasm4s.scaladsl.WasmoSettings +import io.vertx.sqlclient.Row +import play.api.libs.json.{JsObject, Json} + +import java.time.ZoneOffset +import java.util.UUID +import scala.concurrent.Future + +class ConfigurationDatastore(val env: Env) extends Datastore { + + def readId(): Future[UUID] = { + env.postgresql.queryOne(s"""SELECT izanami_id FROM izanami.configuration"""){r => r.optUUID("izanami_id")} + .map(_.get) + + } + def readWasmConfiguration(): Option[WasmoSettings] = { + for( + url <- env.configuration.getOptional[String]("app.wasmo.url"); + clientId <- env.configuration.getOptional[String]("app.wasmo.client-id"); + clientSecret <- env.configuration.getOptional[String]("app.wasmo.client-secret") + ) yield WasmoSettings(url, clientId, clientSecret = clientSecret) + } + + def readOIDCConfiguration(): Option[OIDCConfiguration] = { + for( + clientId <- env.configuration.getOptional[String]("app.openid.client-id"); + clientSecret <- env.configuration.getOptional[String]("app.openid.client-secret"); + authorizeUrl <- env.configuration.getOptional[String]("app.openid.authorize-url"); + tokenUrl <- env.configuration.getOptional[String]("app.openid.token-url"); + redirectUrl <- env.configuration.getOptional[String]("app.openid.redirect-url") + ) yield OIDCConfiguration(clientId=clientId, clientSecret=clientSecret, authorizeUrl=authorizeUrl, tokenUrl=tokenUrl, redirectUrl=redirectUrl) + } + + def readConfiguration(): Future[Either[IzanamiError, IzanamiConfiguration]] = { + env.postgresql + .queryOne("SELECT mailer, invitation_mode, origin_email, anonymous_reporting, anonymous_reporting_date from izanami.configuration") { row => + { + row.optConfiguration() + } + } + .map(o => { + o.toRight(ConfigurationReadError()) + }) + } + + def readFullConfiguration(): Future[Either[IzanamiError, FullIzanamiConfiguration]] = { + env.postgresql + .queryOne(s""" + |SELECT c.mailer, c.invitation_mode, c.origin_email, c.anonymous_reporting, m.configuration, m.name + |FROM izanami.configuration c, izanami.mailers m + |WHERE m.name = c.mailer + |""".stripMargin) { row => + { + for ( + mailProviderConfig <- row.optMailerConfiguration(); + invitationMode <- row.optString("invitation_mode"); + anonymousReporting <- row.optBoolean("anonymous_reporting") + ) + yield FullIzanamiConfiguration( + invitationMode = parseInvitationMode(invitationMode), + mailConfiguration = mailProviderConfig, + originEmail = row.optString("origin_email"), + anonymousReporting=anonymousReporting, + anonymousReportingLastAsked=row.optOffsetDatetime("anonymous_reporting_date").map(_.toInstant) + ) + } + } + .map(o => { + o.toRight(ConfigurationReadError()) + }) + .recover { + case _ => { + Left(InternalServerError()) + } + } + } + + def updateConfiguration(newConfig: IzanamiConfiguration): Future[Option[IzanamiConfiguration]] = { + env.postgresql.queryOne( + s""" + |UPDATE izanami.configuration + |SET mailer=$$1, invitation_mode=$$2, origin_email=$$3, anonymous_reporting=$$4, anonymous_reporting_date=$$5 + |RETURNING * + |""".stripMargin, + List(newConfig.mailer.toString.toUpperCase, newConfig.invitationMode.toString.toUpperCase, newConfig.originEmail.orNull, java.lang.Boolean.valueOf(newConfig.anonymousReporting), newConfig.anonymousReportingLastAsked.map(_.atOffset(ZoneOffset.UTC)).orNull ) + ) { row => + row.optConfiguration() + } + } + + def readMailerConfiguration(mailerType: MailerType): Future[Either[IzanamiError, MailProviderConfiguration]] = { + env.postgresql + .queryOne( + s"""SELECT name, configuration FROM izanami.mailers WHERE name=$$1""", + List(mailerType.toString.toUpperCase) + ) { row => + row.optMailerConfiguration() + } + .map(o => { + o.toRight(ConfigurationReadError()) + }) + } + + def updateMailerConfiguration( + mailProviderConfiguration: MailProviderConfiguration + ): Future[Either[IzanamiError, MailProviderConfiguration]] = { + env.postgresql + .queryOne( + s""" + |UPDATE izanami.mailers + |SET configuration=$$1::JSONB + |WHERE name=$$2 + |RETURNING * + |""".stripMargin, + List( + mailProviderConfiguration match { + case ConsoleMailProvider => "{}" + case MailJetMailProvider(configuration) if configuration.url.isDefined => + Json + .obj( + "url" -> configuration.url.get, + "apiKey" -> configuration.apiKey, + "secret" -> configuration.secret + ) + .toString + case MailJetMailProvider(configuration) => + Json + .obj( + "apiKey" -> configuration.apiKey, + "secret" -> configuration.secret + ) + .toString + case MailGunMailProvider(configuration) if configuration.url.isDefined => + Json + .obj( + "url" -> configuration.url.get, + "apiKey" -> configuration.apiKey, + "region" -> configuration.region + ) + .toString + case MailGunMailProvider(configuration) => + Json + .obj( + "apiKey" -> configuration.apiKey, + "region" -> configuration.region + ) + .toString + case SMTPMailProvider(configuration) => + Json.toJson(configuration).toString + }, + mailProviderConfiguration.mailerType.toString.toUpperCase + ) + ) { row => + row.optMailerConfiguration() + } + .map(o => { + o.toRight(ConfigurationReadError()) + }) + .recover { + case _ => { + Left(InternalServerError()) + } + } + } +} + +object ConfigurationDatastore { + def parseDbMailer(dbMailer: String): MailerType = { + dbMailer match { + case "CONSOLE" => MailerTypes.Console + case "MAILJET" => MailerTypes.MailJet + case "MAILGUN" => MailerTypes.MailGun + case "SMTP" => MailerTypes.SMTP + case _ => throw new RuntimeException(s"Failed to read Mailer (readed ${dbMailer})") + } + } + + def parseInvitationMode(mode: String): InvitationMode = { + mode match { + case "MAIL" => InvitationMode.Mail + case "RESPONSE" => InvitationMode.Response + case _ => throw new RuntimeException(s"Failed to read Invitation mode (readed ${mode})") + } + } +} + +object configurationImplicits { + implicit class ConfigurationRow(val row: Row) extends AnyVal { + def optConfiguration(): Option[IzanamiConfiguration] = { + for ( + mailerStr <- row.optString("mailer"); + invitationModeStr <- row.optString("invitation_mode"); + anonymousReporting <- row.optBoolean("anonymous_reporting") + ) + yield IzanamiConfiguration( + parseDbMailer(mailerStr), + parseInvitationMode(invitationModeStr), + originEmail = row.optString("origin_email"), + anonymousReporting=anonymousReporting, + anonymousReportingLastAsked = row.optOffsetDatetime("anonymous_reporting_date").map(_.toInstant) + ) + } + } + + implicit class MailerConfigurationRow(val row: Row) extends AnyVal { + def optMailerConfiguration(): Option[MailProviderConfiguration] = { + for ( + name <- row.optString("name"); + configuration <- row.optJsObject("configuration") + ) yield parseDbMailer(name) match { + case MailerTypes.MailJet => { + MailJetMailProvider( + configuration + .asOpt[MailJetConfiguration] + .getOrElse(MailJetConfiguration(apiKey = null, secret = null, url = None)) + ) + } + case MailerTypes.MailGun => { + MailGunMailProvider( + configuration + .asOpt[MailGunConfiguration] + .getOrElse(MailGunConfiguration(apiKey = null, url = None)) + ) + } + case MailerTypes.SMTP => { + SMTPMailProvider( + configuration.asOpt[SMTPConfiguration] + .getOrElse(SMTPConfiguration(null, None, None, None, false, false, false)) + ) + } + case MailerTypes.Console => ConsoleMailProvider + } + } + } +} diff --git a/app/fr/maif/izanami/datastores/FeatureContextDatastore.scala b/app/fr/maif/izanami/datastores/FeatureContextDatastore.scala new file mode 100644 index 000000000..4a7770a64 --- /dev/null +++ b/app/fr/maif/izanami/datastores/FeatureContextDatastore.scala @@ -0,0 +1,529 @@ +package fr.maif.izanami.datastores + +import fr.maif.izanami.datastores.FeatureContextDatastore.FeatureContextRow +import fr.maif.izanami.env.Env +import fr.maif.izanami.env.PostgresqlErrors.{FOREIGN_KEY_VIOLATION, RELATION_DOES_NOT_EXISTS, UNIQUE_VIOLATION} +import fr.maif.izanami.env.pgimplicits.EnhancedRow +import fr.maif.izanami.errors._ +import fr.maif.izanami.models.Feature.{activationConditionRead, activationConditionWrite} +import fr.maif.izanami.models.FeatureContext.generateSubContextId +import fr.maif.izanami.models._ +import fr.maif.izanami.utils.Datastore +import fr.maif.izanami.utils.syntax.implicits.BetterSyntax +import fr.maif.izanami.wasm.WasmConfig +import io.otoroshi.wasm4s.scaladsl.WasmSourceKind +import io.vertx.core.json.JsonArray +import io.vertx.pgclient.PgException +import io.vertx.sqlclient.Row +import play.api.libs.json.Json + +import java.util.Objects +import scala.concurrent.Future + +class FeatureContextDatastore(val env: Env) extends Datastore { + def deleteGlobalFeatureContext(tenant: String, path: Seq[String]): Future[Either[IzanamiError, Unit]] = { + val ctxName = path.last + val parentPart = path.init + env.postgresql.executeInTransaction(conn => { + env.postgresql + .queryOne( + s""" + |DELETE FROM global_feature_contexts + |WHERE id=$$1 + |RETURNING * + |""".stripMargin, + List(generateSubContextId(tenant, path)), + schemas = Set(tenant), + conn=Some(conn) + ) { r => Some(()) } + .map(_.toRight(FeatureContextDoesNotExist(path.mkString("/")))) + .flatMap { + case Left(value) => Left(value).future + case Right(value) => env.postgresql.queryRaw( + s"""DELETE FROM feature_context_name_unicity_check_table WHERE parent = $$1 AND context=$$2""", + List(if(parentPart.isEmpty) "" else generateSubContextId(tenant, parentPart), ctxName), + conn=Some(conn) + ){r => Some(())} + .map(_ => Right(())) + } + }) + } + + def createGlobalFeatureContext( + tenant: String, + parents: Seq[String], + featureContext: FeatureContext + ): Future[Either[IzanamiError, FeatureContext]] = { + val id = generateSubContextId(tenant, featureContext.name, parents) + val parentId = if (parents.isEmpty) null else generateSubContextId(tenant, parents) + + env.postgresql.executeInTransaction(conn => { + env.postgresql + .queryOne( + s""" + |INSERT INTO global_feature_contexts (id, name, parent) + |VALUES ($$1, $$2, $$3) + |RETURNING * + |""".stripMargin, + List(id, featureContext.name, parentId), + schemas = Set(tenant), + conn = Some(conn) + ) { row => row.optFeatureContext(global = true) } + .map(o => o.toRight(InternalServerError())) + .flatMap { + case Left(value) => Left(value).future + case Right(ctx) => env.postgresql.queryOne( + s""" + |INSERT INTO feature_context_name_unicity_check_table (parent, context) VALUES ($$1, $$2) RETURNING context + |""".stripMargin, + List(if(Objects.isNull(parentId)) "" else parentId, featureContext.name), + conn = Some(conn) + ) { r => Some(ctx) } + .map(o => o.toRight(InternalServerError())) + .recover { + case f: PgException if f.getSqlState == UNIQUE_VIOLATION => + Left(ConflictWithSameNameLocalContext(name = featureContext.name, parentCtx = parents.mkString("/"))) + case _ => + Left(InternalServerError()) + } + } + }) + + } + + // TODO merge with createFeatureSubContext ? + def createFeatureContext( + tenant: String, + project: String, + featureContext: FeatureContext + ): Future[Either[IzanamiError, FeatureContext]] = { + val id = generateSubContextId(project, featureContext.name) + env.postgresql.executeInTransaction(conn => { + env.postgresql + .queryOne( + s""" + |INSERT INTO feature_contexts (id, name, project) + |VALUES ($$1, $$2, $$3) + |RETURNING * + |""".stripMargin, + List(id, featureContext.name, project), + schemas = Set(tenant), + conn=Some(conn) + ) { row => row.optFeatureContext(global = false) } + .map(o => o.toRight(InternalServerError())) + .flatMap { + case Left(value) => Left(value).future + case Right(ctx) => { + env.postgresql.queryOne( + s""" + |INSERT INTO feature_context_name_unicity_check_table (parent, context) VALUES ($$1, $$2) RETURNING context + |""".stripMargin, + List("", featureContext.name), + conn = Some(conn) + ) { _ => Some(ctx) } + .map(o => o.toRight(InternalServerError())) + .recover { + case f: PgException if f.getSqlState == UNIQUE_VIOLATION => + Left(ConflictWithSameNameGlobalContext(name = featureContext.name)) + case _ => + Left(InternalServerError()) + } + } + } + + + }) + + } + + def createFeatureSubContext( + tenant: String, + project: String, + parents: Seq[String], + name: String + ): Future[Either[IzanamiError, FeatureContext]] = { + if(parents.isEmpty) { + createFeatureContext(tenant, project, FeatureContext(id=null, name, global=true)) + } else { + val id = generateSubContextId(project, name, parents) + val isLocal = env.postgresql + .queryOne( + s"""SELECT id FROM feature_contexts WHERE id=$$1""", + List(generateSubContextId(project, parents)), + schemas = Set(tenant) + ) { r => + Some(()) + } + .map(o => o.fold(false)(_ => true)) + + isLocal.flatMap(local => { + val parentId = + if (parents.isEmpty) null + else if (local) generateSubContextId(project, parents) + else generateSubContextId(tenant, parents) + env.postgresql.executeInTransaction(conn => { + env.postgresql + .queryOne( + s""" + |INSERT INTO feature_contexts (id, name, project, ${if (local) "parent" else "global_parent"}) + |VALUES ($$1, $$2, $$3, $$4) + |RETURNING * + |""".stripMargin, + List(id, name, project, parentId), + conn = Some(conn) + ) { row => row.optFeatureContext(global = false) } + .map(o => o.toRight(InternalServerError())) + .recover { + case f: PgException if f.getSqlState == FOREIGN_KEY_VIOLATION => + Left(FeatureContextDoesNotExist(parents.mkString("/"))) + case ex => + logger.error("Failed to user", ex) + Left(InternalServerError()) + } + .flatMap { + case Left(error) => Left(error).future + case Right(ctx) if !local => { + env.postgresql.queryOne( + s""" + |INSERT INTO feature_context_name_unicity_check_table (parent, context) VALUES ($$1, $$2) RETURNING context + |""".stripMargin, + List(parentId, name), + conn=Some(conn) + ){_ => Some(ctx) } + .map(o => o.toRight(InternalServerError())) + .recover { + case f: PgException if f.getSqlState == UNIQUE_VIOLATION => + Left(ConflictWithSameNameGlobalContext(name=name, parentCtx=parents.mkString("/"))) + case _ => + Left(InternalServerError()) + } + } + case Right(ctx) => Right(ctx).future + } + }, schemas = Set(tenant)) + }) + } + } + + def readStrategyForContext( + tenant: String, + featureContext: Seq[String], + feature: AbstractFeature + ): Future[Option[ContextualFeatureStrategy]] = { + val possibleContextPaths = featureContext.foldLeft(Seq(): Seq[Seq[String]])((acc, next) => { + val newElement = acc.lastOption.map(last => last.appended(next)).getOrElse(Seq(next)) + acc.appended(newElement) + }) + + val possibleContextIds = possibleContextPaths.map(path => { + generateSubContextId(feature.project, path.last, path.dropRight(1)) + }).concat(possibleContextPaths.map(path => { + generateSubContextId(tenant, path.last, path.dropRight(1)) + })) + + env.postgresql.queryOne( + s""" + |SELECT gf.conditions, gf.enabled, ws.config + |FROM feature_contexts_strategies gf + |LEFT OUTER JOIN wasm_script_configurations ws ON gf.script_config=ws.id + |WHERE gf.project = $$1 + |AND gf.feature = $$2 + |AND gf.context=ANY($$3) + |ORDER BY length(gf.context) desc + |limit 1 + |""".stripMargin, + List(feature.project, feature.name, possibleContextIds.toArray), + schemas=Set(tenant) + ) { row => + { + row.optStrategy(feature.name) + } + } + } + + def readGlobalFeatureContexts(tenant: String): Future[Seq[FeatureContext]] = { + env.postgresql.queryAll( + s""" + |SELECT c.name, c.parent, c.id, NULL as project, COALESCE( + | json_agg(json_build_object('feature', s.feature, 'enabled', s.enabled, 'conditions', s.conditions, 'id', f.id, 'project', f.project, 'description', f.description , 'wasm', w.config)) FILTER (WHERE s.feature IS NOT NULL) , '[]' + |) as overloads + |FROM global_feature_contexts c + |LEFT JOIN feature_contexts_strategies s ON s.global_context=c.id + |LEFT JOIN features f ON f.name=s.feature + |LEFT JOIN wasm_script_configurations w ON w.id=s.script_config + |GROUP BY (c.name, c.parent, c.id) + |""".stripMargin, + List(), + schemas=Set(tenant) + ) { row => row.optFeatureContext(global=true) } + } + + def readAllLocalFeatureContexts(tenant: String): Future[Seq[FeatureContext]] = { + env.postgresql.queryAll( + s""" + |SELECT name, parent, id, true as global, null as project + |FROM global_feature_contexts + |UNION ALL + |SELECT name, parent, id, false as global, project + |FROM feature_contexts + |""".stripMargin, + schemas = Set(tenant)) + {r => + for( + id <- r.optString("id"); + name <- r.optString("name"); + global <- r.optBoolean("global") + ) yield FeatureContext(id, name, r.optString("parent").orNull, global=global, project=r.optString("project")) + } + } + + def readFeatureContexts(tenant: String, project: String): Future[Seq[FeatureContext]] = { + env.postgresql.queryAll( + s""" + |SELECT c.name, COALESCE(c.parent, c.global_parent) as parent, c.id, c.project, COALESCE( + | json_agg(json_build_object('feature', s.feature, 'enabled', s.enabled, 'conditions', s.conditions, 'id', f.id, 'description', f.description, 'project', f.project, 'wasm', w.config)) FILTER (WHERE s.feature IS NOT NULL) , '[]' + |) as overloads + |FROM feature_contexts c + |LEFT JOIN feature_contexts_strategies s ON s.context=c.id + |LEFT JOIN features f ON f.name=s.feature + |LEFT JOIN wasm_script_configurations w ON w.id=s.script_config + |WHERE c.project=$$1 + |GROUP BY (c.name, parent, c.id, c.project) + |UNION ALL + |SELECT c.name, c.parent, c.id, NULL as project, COALESCE( + | json_agg(json_build_object('feature', s.feature, 'enabled', s.enabled, 'conditions', s.conditions, 'id', f.id, 'description', f.description, 'project', f.project, 'wasm', w.config)) FILTER (WHERE s.feature IS NOT NULL) , '[]' + |) as overloads + |FROM global_feature_contexts c + |LEFT JOIN feature_contexts_strategies s ON s.global_context=c.id + |LEFT JOIN features f ON f.name=s.feature + |LEFT JOIN wasm_script_configurations w ON w.id=s.script_config + |GROUP BY (c.name, c.parent, c.id) + |""".stripMargin, + List(project), + schemas=Set(tenant) + ) { row => row.optFeatureContext(global=row.optString("project").isEmpty) } + } + + def deleteContext(tenant: String, project: String, path: Seq[String]): Future[Either[IzanamiError, Unit]] = { + val ctxName = path.last + val parentPart = path.init + env.postgresql.executeInTransaction(conn => { + env.postgresql + .queryOne( + s""" + |DELETE FROM feature_contexts + |WHERE id=$$1 + |RETURNING * + |""".stripMargin, + List(generateSubContextId(project, path)), + schemas = Set(tenant), + conn=Some(conn) + ) { r => Some(()) } + .map(_.toRight(FeatureContextDoesNotExist(path.mkString("/")))) + .flatMap { + case Left(value) => Left(value).future + case Right(value) => { + env.postgresql.queryRaw( + s"""DELETE FROM feature_context_name_unicity_check_table WHERE parent = $$1 AND context=$$2""", + List(if (parentPart.isEmpty) "" else generateSubContextId(project, parentPart), ctxName), + conn = Some(conn) + ){_ => Some(())} + .map(_ => Right(())) + } + } + }) + } + + def deleteFeatureStrategy( + tenant: String, + project: String, + path: Seq[String], + feature: String + ): Future[Either[IzanamiError, Unit]] = { + val isLocal = env.postgresql + .queryOne( + s"""SELECT id FROM feature_contexts WHERE id=$$1""", + List(generateSubContextId(project, path)), + schemas=Set(tenant) + ) { r => + Some(()) + } + .map(o => o.fold(false)(_ => true)) + isLocal.flatMap(local => + env.postgresql + .queryOne( + s""" + |DELETE FROM feature_contexts_strategies + |WHERE project=$$1 AND context=$$2 AND feature=$$3 + |RETURNING feature + |""".stripMargin, + List(project, if(local)generateSubContextId(project, path) else generateSubContextId(tenant, path), feature), + schemas=Set(tenant) + ) { r => Some(()) } + .map(_.toRight(FeatureOverloadDoesNotExist(project, path.mkString("/"), feature))) + ) + } + + def updateFeatureStrategy( + tenant: String, + project: String, + path: Seq[String], + feature: String, + strategy: ContextualFeatureStrategy + ): Future[Either[IzanamiError, Unit]] = { + // TODO factorize this + val isLocal = env.postgresql + .queryOne( + s"""SELECT id FROM feature_contexts WHERE id=$$1""", + List(generateSubContextId(project, path)), + schemas=Set(tenant) + ) { r => + Some(()) + } + .map(o => o.fold(false)(_ => true)) + isLocal.flatMap(local => + strategy match { + case ClassicalFeatureStrategy(enabled, conditions, _) => + env.postgresql + .queryOne( + s""" + |INSERT INTO feature_contexts_strategies (project, ${if (local) "local_context" + else "global_context"}, conditions, enabled, feature) VALUES($$1,$$2,$$3,$$4,$$5) + |ON CONFLICT(project, context, feature) DO UPDATE + |SET conditions=EXCLUDED.conditions, enabled=EXCLUDED.enabled, script_config=NULL + |RETURNING 1 + |""".stripMargin, + List( + project, + if (local) generateSubContextId(project, path.last, path.dropRight(1)) + else generateSubContextId(tenant, path.last, path.dropRight(1)), + new JsonArray(Json.toJson(conditions).toString()), + java.lang.Boolean.valueOf(enabled), + feature + ), + schemas=Set(tenant) + ) { _ => Some(()) } + .map(u => u.toRight(ProjectOrFeatureDoesNotExists(project, feature))) + .recover { + case f: PgException if f.getSqlState == RELATION_DOES_NOT_EXISTS => Left(TenantDoesNotExists(tenant)) + case f: PgException if f.getSqlState == FOREIGN_KEY_VIOLATION => + Left(FeatureContextDoesNotExist(path.mkString("/"))) + case _ => Left(InternalServerError()) + } + case WasmFeatureStrategy(enabled, wasmConfig, feature) => + env.postgresql.executeInTransaction(conn => { + val maybeWasmScriptQuery = if (wasmConfig.source.kind != WasmSourceKind.Local) { + env.datastores.features.createWasmScriptIfNeeded(tenant, wasmConfig, Some(conn)) + } else { + Right(wasmConfig.name).future + } + maybeWasmScriptQuery + .flatMap { + case Left(err) => Left(err).future + case Right(id) => { + env.postgresql + .queryOne( + s""" + |INSERT INTO feature_contexts_strategies (project, ${if (local) "local_context" + else "global_context"}, script_config, enabled, feature) VALUES($$1,$$2,$$3,$$4,$$5) + |ON CONFLICT(project, context, feature) DO UPDATE + |SET script_config=EXCLUDED.script_config, enabled=EXCLUDED.enabled, conditions=NULL + |RETURNING 1 + |""".stripMargin, + List( + project, + if (local) generateSubContextId(project, path.last, path.dropRight(1)) + else generateSubContextId(tenant, path.last, path.dropRight(1)), + id, + java.lang.Boolean.valueOf(enabled), + feature + ), + schemas=Set(tenant), + conn=Some(conn) + ) { _ => Some(()) } + .map(o => o.toRight(InternalServerError())) + } + } + }, schemas = Set(tenant)) + } + ) + + } +} + +object FeatureContextDatastore { + implicit class FeatureContextRow(val row: Row) extends AnyVal { + def optFeatureContext(global: Boolean): Option[FeatureContext] = { + + val features = row + .optJsArray("overloads") + .toSeq + .flatMap(array => + array.value.flatMap(jsObject => { + val maybeConditions = (jsObject \ "conditions").asOpt[Set[ActivationCondition]] + val maybeScriptConfig = (jsObject \ "wasm").asOpt(WasmConfig.format) + + for ( + name <- (jsObject \ "feature").asOpt[String]; + enabled <- (jsObject \ "enabled").asOpt[Boolean]; + id <- (jsObject \ "id").asOpt[String]; + description <- (jsObject \ "description").asOpt[String]; + project <- (jsObject \ "project").asOpt[String] + ) + yield { + (maybeScriptConfig, maybeConditions) match { + case (Some(wasmConfig), _) => + Some(WasmFeature( + name = name, + enabled = enabled, + id = id, + project = project, + wasmConfig = wasmConfig, + description=description + )) + case (None, Some(conditions)) => + Some(Feature( + name = name, + enabled = enabled, + id = id, + project = project, + conditions = conditions, + description=description + )) + case _ => None + } + }.toSeq + }) + ) + .flatMap(o => o.toSeq) + Some( + FeatureContext( + id = row.getString("id"), + name = row.getString("name"), + parent = row.getString("parent"), + project=if(global) None else row.optString("project"), + overloads = features, + global= global + ) + ) + } + + def optStrategy(feature: String): Option[ContextualFeatureStrategy] = { + val maybeConditions = row + .optJsArray("conditions") + .map(arr => arr.value.map(v => v.as[ActivationCondition]).toSet) + .getOrElse(Set.empty) + val maybeConfig = row.optJsObject("config").flatMap(obj => obj.asOpt(WasmConfig.format)) + + row + .optBoolean("enabled") + .map(enabled => { + (maybeConfig, maybeConditions) match { + case (Some(config), _) => WasmFeatureStrategy(enabled, config, feature) + case (_, conditions) => ClassicalFeatureStrategy(enabled, conditions, feature) + } + }) + } + } + +} diff --git a/app/fr/maif/izanami/datastores/FeaturesDatastore.scala b/app/fr/maif/izanami/datastores/FeaturesDatastore.scala new file mode 100644 index 000000000..01c2967ea --- /dev/null +++ b/app/fr/maif/izanami/datastores/FeaturesDatastore.scala @@ -0,0 +1,1418 @@ +package fr.maif.izanami.datastores + +import fr.maif.izanami.datastores.featureImplicits.FeatureRow +import fr.maif.izanami.env.Env +import fr.maif.izanami.env.PostgresqlErrors.{ + FOREIGN_KEY_VIOLATION, + NOT_NULL_VIOLATION, + RELATION_DOES_NOT_EXISTS, + UNIQUE_VIOLATION +} +import fr.maif.izanami.env.pgimplicits.EnhancedRow +import fr.maif.izanami.errors._ +import fr.maif.izanami.models.Feature.{ + activationConditionRead, + activationConditionWrite, + legacyActivationConditionRead, + legacyCompatibleConditionWrites +} +import fr.maif.izanami.models._ +import fr.maif.izanami.utils.Datastore +import fr.maif.izanami.utils.syntax.implicits.{BetterJsValue, BetterListEither, BetterSyntax} +import fr.maif.izanami.v1.V1FeatureEvents +import fr.maif.izanami.wasm.{WasmConfig, WasmConfigWithFeatures, WasmScriptAssociatedFeatures} +import fr.maif.izanami.web.ImportController.{Fail, ImportConflictStrategy, MergeOverwrite, Skip} +import io.otoroshi.wasm4s.scaladsl.WasmSourceKind +import io.vertx.core.json.{JsonArray, JsonObject} +import io.vertx.core.shareddata.ClusterSerializable +import io.vertx.pgclient.PgException +import io.vertx.sqlclient.{Row, SqlConnection} +import org.postgresql.xml.LegacyInsecurePGXmlFactoryFactory +import play.api.libs.json.{JsObject, JsValue, Json} + +import java.lang +import java.util.UUID +import scala.collection.immutable +import scala.collection.mutable.ArrayBuffer +import scala.concurrent.Future +import scala.reflect.ClassTag + +sealed trait EventType + +case object FeatureCreated extends EventType { + override def toString = "FEATURE_CREATED" +} +case object FeatureDeleted extends EventType { + override def toString = "FEATURE_DELETED" +} +case object FeatureUpdated extends EventType { + override def toString = "FEATURE_UPDATED" +} + +class FeaturesDatastore(val env: Env) extends Datastore { + + def emitEvent( + tenant: String, + id: String, + eventType: EventType, + sourceProject: String, + targetProject: Option[String] = None, + conn: SqlConnection + ): Future[Unit] = { + val payload = Json + .obj( + "id" -> id, + "type" -> eventType.toString, + "project" -> sourceProject + ) + .applyOnWithOpt(targetProject)((json, project) => json ++ Json.obj("newProject" -> project)) + env.postgresql + .queryOne( + s"""SELECT pg_notify($$1, $$2)""", + List(s"$tenant-features", payload.toString), + conn = Some(conn) + ) { r => Some(()) } + .map(_ => ()) + } + + def findFeatureMatching( + tenant: String, + pattern: String, + clientId: String, + count: Int, + page: Int + ): Future[(Int, Seq[AbstractFeature])] = { + val countQuery = env.postgresql.queryOne( + s""" + |select count(f.id) as count + |from features f + |left join apikeys a on a.clientid=$$1 + |left join apikeys_projects ap on (ap.project=f.project and ap.apikey=a.name) + |where f.id LIKE $$2 + |and (f.conditions is null or f.conditions is json object) + |and (ap.project is not null or a.admin=true) + |""".stripMargin, + List(clientId, pattern.replaceAll("\\*", "%")), + schemas = Set(tenant) + ) { r => r.optInt("count") } + + val dataQuery = env.postgresql.queryAll( + s"""select f.*, s.config AS wasm, COALESCE(json_agg(ft.tag) FILTER (WHERE ft.tag IS NOT NULL), '[]') AS tags + |from features f + |left join features_tags ft + |on ft.feature = f.id + |left join wasm_script_configurations s + |on s.id = f.script_config + |left join apikeys a on a.clientid=$$1 + |left join apikeys_projects ap on (ap.project=f.project and ap.apikey=a.name) + |where f.id LIKE $$2 + |and (f.conditions is null or f.conditions is json object) + |and (ap.project is not null or a.admin=true) + |group by f.id, wasm + |order by f.id + |limit $$3 + |offset $$4""".stripMargin, + List(clientId, pattern.replaceAll("\\*", "%"), Integer.valueOf(count), Integer.valueOf((page - 1) * count)), + schemas = Set(tenant) + ) { r => r.optFeature() } + + for ( + count <- countQuery; + features <- dataQuery + ) yield { + (count.getOrElse(features.size), features) + } + } + + def applyPatch(tenant: String, operations: Seq[FeaturePatch]): Future[Unit] = { + env.postgresql.executeInTransaction( + conn => { + val eventualId: Future[Unit] = Future + .sequence(operations.map { + case EnabledFeaturePatch(value, id) => { + env.postgresql + .queryOne( + s"""UPDATE features SET enabled=$$1 WHERE id=$$2 RETURNING id, name, project, enabled""", + List(java.lang.Boolean.valueOf(value), id), + conn = Some(conn) + ) { r => + for ( + id <- r.optString("id"); + name <- r.optString("name"); + project <- r.optString("project"); + enabled <- r.optBoolean("enabled") + ) yield (id, name, project, enabled) + } + .flatMap { + case Some((id, name, project, enabled)) => + emitEvent( + tenant, + id = id, + eventType = FeatureUpdated, + sourceProject = project, + conn = conn + ) + case None => Future.successful(()) + } + } + case RemoveFeaturePatch(id) => { + env.postgresql + .queryOne( + s"""DELETE FROM features WHERE id=$$1 RETURNING id, name, project, enabled""", + List(id), + conn = Some(conn) + ) { r => + for ( + id <- r.optString("id"); + name <- r.optString("name"); + project <- r.optString("project"); + enabled <- r.optBoolean("enabled") + ) yield (id, name, project, enabled) + } + .flatMap { + case Some((id, name, project, enabled)) => + emitEvent( + tenant, + id = id, + eventType = FeatureDeleted, + sourceProject = project, + conn = conn + ) + case None => Future.successful(()) + } + } + }) + .map(_ => ()) + eventualId + }, + schemas = Set(tenant) + ) + } + + def findByIdForKey( + tenant: String, + id: String, + contexts: Seq[String], + clientId: String, + clientSecret: String + ): Future[Option[AbstractFeature]] = { + val possibleContextPaths = contexts + .foldLeft(Seq(): Seq[Seq[String]])((acc, next) => { + val newElement = acc.lastOption.map(last => last.appended(next)).getOrElse(Seq(next)) + acc.appended(newElement) + }) + .map(_.mkString("_")) + val needContexts = contexts.nonEmpty + val params = if (needContexts) List(clientId, id, possibleContextPaths.toArray) else List(clientId, id) + + env.postgresql + .queryAll( + s""" + |SELECT + | k.clientsecret, + | ${if (needContexts) s"fcs.context_path," else "null as context_path,"} + | json_build_object( + | 'name', f.name, + | 'project', f.project, + | 'description', f.description, + | 'id', f.id)::jsonb || + | ${if (needContexts) s"""(CASE + | WHEN fcs.enabled IS NOT NULL THEN + | json_build_object( + | 'enabled', fcs.enabled, + | 'config', ow.config, + | 'conditions', fcs.conditions + | )::jsonb + | ELSE""" else ""} + | json_build_object( + | 'enabled', f.enabled, + | 'config', w.config, + | 'conditions', f.conditions + | )::jsonb + | ${if (needContexts) s"END)" else ""} as feature + |FROM features f + |${if (needContexts) + s"LEFT JOIN feature_contexts_strategies fcs ON fcs.feature=f.name AND fcs.context_path = ANY($$3) LEFT JOIN wasm_script_configurations ow ON fcs.script_config=ow.id" + else ""} + |INNER JOIN apikeys k ON (k.clientid=$$1 AND k.enabled=true) + |LEFT JOIN wasm_script_configurations w ON w.id=f.script_config + |LEFT JOIN apikeys_projects kp ON (kp.apikey=k.name AND kp.project=f.project) + |WHERE f.id=$$2 + |AND (kp.apikey IS NOT NULL OR k.admin=TRUE) + |""".stripMargin, + params, + schemas = Set(tenant) + ) { r => + { + for ( + _ <- r.optString("clientsecret") + .filter(hashed => clientSecret == hashed); // TODO put this check in the above query + jsonFeature <- r.optJsObject("feature"); + js <- (jsonFeature \ "config") + .asOpt[JsValue] + .map(js => jsonFeature.as[JsObject] + ("wasmConfig" -> js)) + .orElse(Some(jsonFeature)); + feature <- Feature.readFeature(js).asOpt + ) yield (r.optString("context_path"), feature) + } + } + .map(ls => + ls.sortWith((f1, f2) => { + (f1, f2) match { + case ((None, _), _) => false + case (_, (None, _)) => true + case ((Some(ctx1), _), (Some(ctx2), _)) if ctx1.length > ctx2.length => true + case _ => false + } + }).headOption + .map(t => t._2) + ) + } + + def searchFeature(tenant: String, tags: Set[String]): Future[Seq[AbstractFeature]] = { + val hasTags = tags.nonEmpty + env.postgresql + .queryOne( + s""" + |select COALESCE( + | json_agg(row_to_json(f.*)::jsonb + | || (json_build_object('tags', ( + | array( + | SELECT ft.tag + | FROM features_tags ft + | WHERE ft.feature = f.id + | GROUP BY ft.tag + | ) + | ), 'wasmConfig', ( + | select w.config FROM wasm_script_configurations w where w.id = f.script_config + | )))::jsonb) + | FILTER (WHERE f.id IS NOT NULL), '[]' + |) as "features" + |from features f${if (hasTags) { + s""", features_tags ft + |WHERE ft.feature = f.id + |AND ft.tag = ANY($$1)""" + } else ""} + |""".stripMargin, + if (hasTags) List(tags.toArray) else List(), + schemas = Set(tenant) + ) { r => + r.optJsArray("features") + .map(arr => arr.value.toSeq.map(js => Feature.readFeature(js).asOpt).flatMap(_.toSeq)) + } + .map(o => o.getOrElse(Seq())) + } + + def readScriptConfig(tenant: String, path: String): Future[Option[WasmConfig]] = { + env.postgresql + .queryOne( + s""" + |SELECT config + |FROM wasm_script_configurations + |WHERE config #>> '{source,path}' = $$1 + |""".stripMargin, + List(path), + schemas = Set(tenant) + ) { row => { row.optJsObject("config") } } + .map(o => o.map(jsObj => jsObj.as[WasmConfig](WasmConfig.format))) + } + + def findFeaturesProjects(tenant: String, ids: Set[String]): Future[Seq[String]] = { + env.postgresql.queryAll( + s""" + |SELECT DISTINCT project FROM features WHERE id=ANY($$1) + |""".stripMargin, + List(ids.toArray), + schemas = Set(tenant) + ) { r => r.optString("project") } + } + + def findById(tenant: String, id: String): Future[Either[IzanamiError, Option[AbstractFeature]]] = { + env.postgresql + .queryOne( + s"""select f.*, s.config AS wasm, COALESCE(json_agg(ft.tag) FILTER (WHERE ft.tag IS NOT NULL), '[]') AS tags + |from features f + |left join features_tags ft + |on ft.feature = f.id + |left join wasm_script_configurations s + |on s.id = f.script_config + |where f.id = $$1 + |group by f.id, wasm""".stripMargin, + List(id), + schemas = Set(tenant) + ) { row => row.optFeature() } + .map(o => Right(o)) + .recover { + case f: PgException if f.getSqlState == RELATION_DOES_NOT_EXISTS => Left(TenantDoesNotExists(tenant)) + case _ => Left(InternalServerError()) + } + } + + def findByIdForKeyWithoutCheck( + tenant: String, + id: String, + clientId: String + ): Future[Either[IzanamiError, Option[AbstractFeature]]] = { + env.postgresql + .queryOne( + s"""select (ap.project IS NOT NULL OR k.admin=TRUE) AS authorized, f.*, s.config AS wasm, COALESCE(json_agg(ft.tag) FILTER (WHERE ft.tag IS NOT NULL), '[]') AS tags + |from features f + |left join features_tags ft + |on ft.feature = f.id + |left join wasm_script_configurations s + |on s.id = f.script_config + |inner join apikeys k + |on k.clientid=$$2 + |left join apikeys_projects ap + |on (ap.apikey=k.name AND ap.project=f.project) + |where f.id = $$1 + |group by f.id, k.admin, wasm, ap.project""".stripMargin, + List(id, clientId), + schemas = Set(tenant) + ) { row => + { + row + .optBoolean("authorized") + .map(authorized => { + if (authorized) { + row.optFeature().toRight(InternalServerError()) + } else { + Left(NotEnoughRights()) + } + }) + } + } + .map { + case Some(Right(feature)) => Right(Some(feature)) + case Some(Left(error)) => Left(error) + case None => Right(None) + } + .recover { + case f: PgException if f.getSqlState == RELATION_DOES_NOT_EXISTS => Left(TenantDoesNotExists(tenant)) + case _ => Left(InternalServerError()) + } + } + + case class FeatureWithOverloads(feature: AbstractFeature, overloads: Map[String, ContextualFeatureStrategy]) + + def doFindByRequestForKey( + tenant: String, + request: FeatureRequest, + clientId: String, + clientSecret: String, + conditions: Boolean + ): Future[Either[IzanamiError, Map[UUID, Map[String, Iterable[(Option[String], AbstractFeature)]]]]] = { + val possibleContextPaths = request.context + .foldLeft(Seq(): Seq[Seq[String]])((acc, next) => { + val newElement = acc.lastOption.map(last => last.appended(next)).getOrElse(Seq(next)) + acc.appended(newElement) + }) + .map(_.mkString("_")) + + val needTags = request.allTagsIn.nonEmpty || request.noTagIn.nonEmpty || request.oneTagIn.nonEmpty; + val needContexts = request.context.nonEmpty || conditions + + val params = if (needContexts && !conditions) { + List(clientId, clientSecret, request.projects.toArray, request.features.toArray, possibleContextPaths.toArray) + } else { + List(clientId, clientSecret, request.projects.toArray, request.features.toArray) + } + + env.postgresql + .queryAll( + s""" + |SELECT + | f.id, + | p.id as pid, + | f.enabled, + | f.name, + | f.project, + | f.conditions, + | f.description, + | f.script_config, + | f.metadata, + | w.config as wasm + | ${if (needContexts) """, + | COALESCE(json_object_agg(fcs.context_path, json_build_object( + | 'id', f.id, + | 'name', f.name, + | 'project', f.project, + | 'description', f.description, + | 'enabled', fcs.enabled, + | 'conditions', fcs.conditions, + | 'context', fcs.context, + | 'wasmConfig', ow.config, + | 'context_path', fcs.context_path)) FILTER(WHERE fcs.enabled IS NOT NULL), '{}'::json) AS overloads + | """ else ""} + | ${if (needTags) ",COALESCE(json_agg(t.id) FILTER(WHERE t.id IS NOT NULL), '[]') as tags" else ""} + | FROM projects p + | LEFT JOIN features f on f.project = p.name + | ${if (needTags)""" + | LEFT JOIN features_tags ft ON f.id=ft.feature + | LEFT JOIN tags t ON t.name=ft.tag""" else ""} + | ${if (needContexts)s""" + | LEFT JOIN feature_contexts_strategies fcs ON fcs.feature=f.name ${if (!conditions)s"AND fcs.context_path = ANY($$5)" else ""} + | LEFT JOIN wasm_script_configurations ow ON fcs.script_config=ow.id""".stripMargin else ""} + | LEFT JOIN wasm_script_configurations w ON w.id=f.script_config + | INNER JOIN apikeys k ON (k.clientid=$$1 AND k.clientsecret=$$2 AND k.enabled=true) + | LEFT JOIN apikeys_projects kp ON (kp.apikey=k.name AND kp.project=p.name) + | WHERE (f.project = p.name OR f.name IS NULL) + | AND (kp.apikey IS NOT NULL OR k.admin=TRUE) + | AND (p.id=ANY($$3) OR f.id=ANY($$4)) + | GROUP BY f.id, pid, w.config + |""".stripMargin, + params, + schemas = Set(tenant) + ) {r => { + r.optFeature().filter(f => { + if(needTags) { + val tags = f.tags.map(t => UUID.fromString(t)) + val specificFeatureRequest = request.features.contains(f.id) + val allTagsInOk = request.allTagsIn.subsetOf(tags) + val oneTagInOk = request.oneTagIn.isEmpty || request.oneTagIn.exists(u => tags.contains(u)) + val noTagsInOk = !request.noTagIn.exists(u => tags.contains(u)) + + specificFeatureRequest || (allTagsInOk && oneTagInOk && noTagsInOk) + } else { + true + } + }).flatMap(f => { + if(needContexts) { + r.optJsObject("overloads") + .map(jsObject => { + val objByContext = jsObject.as[Map[String, JsObject]] + val overloadByPath: Map[Option[String], AbstractFeature] = objByContext.map { case (ctx, jsObject) => (ctx, Feature.readFeature(jsObject).asOpt) }.filter { + case (_, None) => false + case _ => true + }.map { case (ctx, optionF) => (Some(ctx), optionF.get) } + + (r.optUUID("pid").get, (f.id, overloadByPath + (None -> f))) + }) + } else { + Some((r.optUUID("pid").get, (f.id, Map(None -> f)))) + } + }) + }}.map(l => { + val featureByProjects = l.groupBy(t => t._1).map { case (k, v) => (k, v.map(t => t._2).toMap) } + Right(featureByProjects) + }) + .recover { + case f: PgException if f.getSqlState == RELATION_DOES_NOT_EXISTS => Left(InvalidCredentials()) + case _ => Left(InternalServerError()) + } + } + + def findByRequestForKey( + tenant: String, + request: FeatureRequest, + clientId: String, + clientSecret: String + ): Future[Either[IzanamiError, Map[UUID, Seq[AbstractFeature]]]] = { + doFindByRequestForKey( + tenant, + request, + clientId, + clientSecret, + conditions = false + ).map { + case Left(err) => Left(err) + case Right(l) => { + Right(l.map { + case (projectId, featuresById) => { + ( + projectId, + featuresById.map { + case (id, featuresWithContext) => { + featuresWithContext.toSeq + .sortWith { + case ((firstContext, feature), (secondContext, feature2)) => { + (firstContext, secondContext) match { + case (None, _) => false + case (_, None) => true + case (Some(ctx1), Some(ctx2)) if ctx1.length > ctx2.length => true + case _ => false + } + } + } + .head + ._2 + } + }.toSeq + ) + } + }) + } + } + } + + def findByRequestV2( + tenant: String, + request: FeatureRequest, + contexts: Seq[String], + user: String + ): Future[Map[UUID, Seq[AbstractFeature]]] = { + val possibleContextPaths = contexts + .foldLeft(Seq(): Seq[Seq[String]])((acc, next) => { + val newElement = acc.lastOption.map(last => last.appended(next)).getOrElse(Seq(next)) + acc.appended(newElement) + }) + .map(seq => seq.mkString("_")) + + env.postgresql + .queryAll( + s""" + |WITH filtered_features AS ( + | SELECT + | f.id, + | p.id as pid, + | f.enabled, + | f.name, + | f.project, + | f.conditions, + | f.script_config, + | f.description, + | w.config, + | fcs.enabled as overload_enabled, + | fcs.conditions as overload_conditions, + | fcs.context as overload_context, + | ow.config as overload_config, + | fcs.context_path, + | COALESCE(json_agg(t.id) FILTER(WHERE t.id IS NOT NULL), '[]'::json) as tags + | FROM + | izanami.sessions s + | JOIN izanami.users u ON s.username=u.username + | LEFT JOIN izanami.users_tenants_rights utr ON utr.username=u.username + | LEFT JOIN users_projects_rights upr ON upr.username=s.username, + | projects p + | LEFT JOIN features f on f.project = p.name + | LEFT JOIN features_tags ft ON f.id=ft.feature + | LEFT JOIN tags t ON t.name=ft.tag + | LEFT JOIN feature_contexts_strategies fcs ON fcs.feature=f.name AND fcs.context_path = ANY($$4) + | LEFT JOIN wasm_script_configurations ow ON fcs.script_config=ow.id + | LEFT JOIN wasm_script_configurations w ON w.id=f.script_config + | WHERE s.username=$$1 + | AND (f.project = p.name OR f.name IS NULL) + | AND ( + | (upr.project=p.name AND upr.username=s.username) + | OR (u.admin OR utr.level = 'ADMIN') + | ) + | AND (p.id=ANY($$2) OR f.id=ANY($$3)) + | GROUP BY f.id, pid, w.config, ow.config, fcs.enabled, fcs.conditions, fcs.context, fcs.context_path + |) SELECT filtered_features.pid AS project_id, + | COALESCE(json_agg(json_build_object( + | 'name', filtered_features.name, + | 'project', filtered_features.project, + | 'tags', filtered_features.tags, + | 'context', filtered_features.context_path, + | 'description', filtered_features.description, + | 'id', filtered_features.id)::jsonb || + | (CASE + | WHEN filtered_features.overload_enabled IS NOT NULL THEN + | json_build_object( + | 'enabled', filtered_features.overload_enabled, + | 'config', filtered_features.overload_config, + | 'conditions', filtered_features.overload_conditions + | )::jsonb + | ELSE + | json_build_object( + | 'enabled', filtered_features.enabled, + | 'config', filtered_features.config, + | 'conditions', filtered_features.conditions + | )::jsonb + | END) + | ) FILTER(WHERE filtered_features.name IS NOT NULL), '[]'::json) as features + |FROM filtered_features + |GROUP BY filtered_features.pid; + |""".stripMargin, + List( + user, + request.projects.toArray, + request.features.toArray, + possibleContextPaths.toArray + ), + schemas = Set(tenant) + ) { r => + { + r.optUUID("project_id") + .map(p => { + val tuple: (UUID, Seq[AbstractFeature]) = ( + p, + r.optJsArray("features") + .toSeq + .flatMap(maybeArray => maybeArray.value) + .groupBy(jsObj => (jsObj \ "id").as[String]) + .values + .map(featureDuplicates => + featureDuplicates + .sortWith((first, second) => { + def contextSize(jsValue: JsValue): Int = + (jsValue \ "context").asOpt[String].map(_.split("_")).map(_.length).getOrElse(0) + val firstContextSize = contextSize(first) + val secondContextSize = contextSize(second) + + if (firstContextSize == 0) true + else if (secondContextSize == 0) false + else if (firstContextSize < secondContextSize) false + else true + }) + .head + ) + .flatMap(f => { + Feature + .readFeature( + (f \ "config").asOpt[JsValue].map(js => f.as[JsObject] + ("wasmConfig" -> js)).getOrElse(f) + ) + .asOpt + .toSeq + }) + .filter(f => + request.features.contains(f.id) || request.allTagsIn.subsetOf(f.tags.map(UUID.fromString)) + ) + .filter(f => + request.features.contains(f.id) || request.oneTagIn.isEmpty || request.oneTagIn + .exists(u => f.tags.contains(u.toString)) + ) + .filter(f => + request.features.contains(f.id) || !request.noTagIn.exists(u => f.tags.contains(u.toString)) + ) + .toSeq + ) + tuple + }) + } + } + .map(_.toMap) + } + + def createFeaturesAndProjects( + tenant: String, + features: Iterable[AbstractFeature], + conflictStrategy: ImportConflictStrategy, + user: String, + conn: Option[SqlConnection] + ): Future[Either[List[IzanamiError], Unit]] = { + // TODO return seq[Error] instead of a single one + if (features.isEmpty) { + Future.successful(Right(())) + } else { + def callback(conn: SqlConnection): Future[Either[List[IzanamiError], Unit]] = { + env.datastores.projects + .createProjects(tenant, features.map(_.project).toSet, conflictStrategy, user, conn = conn.some) + .flatMap { + case Left(error) => Future.successful(Left(List(error))) + case _ => createBulk(tenant, features, conflictStrategy, conn) + } + } + + conn.map(callback).getOrElse(env.postgresql.executeInTransaction(conn => callback(conn))) + } + } + + def createBulk( + tenant: String, + features: Iterable[AbstractFeature], + conflictStrategy: ImportConflictStrategy, + conn: SqlConnection + ): Future[Either[List[IzanamiError], Unit]] = { + def insertFeatures[T <: ClusterSerializable]( + params: ( + Array[String], + Array[String], + Array[String], + Array[java.lang.Boolean], + Array[T], + Array[Object], + Array[String] + ) + ): Future[Either[InternalServerError, List[(String, String)]]] = { + env.postgresql + .queryAll( + s"""INSERT INTO features (id, name, project, enabled, conditions, metadata, description) + |VALUES (unnest($$1::text[]), unnest($$2::text[]), unnest($$3::text[]), unnest($$4::boolean[]), unnest($$5::jsonb[]), unnest($$6::jsonb[]), unnest($$7::text[])) + ${conflictStrategy match { + case Fail => "" + case Skip => " ON CONFLICT DO NOTHING" + case MergeOverwrite => + """ ON CONFLICT (name, project) DO UPDATE SET id=excluded.id, name=excluded.name, project=excluded.project, enabled=excluded.enabled, conditions=excluded.conditions, metadata=excluded.metadata, description=excluded.description, script_config=null + |""".stripMargin + }} + returning id, project""".stripMargin, + params.productIterator.toList.map(a => a.asInstanceOf[AnyRef]), + conn = Some(conn), + schemas = Set(tenant) + ) { row => + for ( + id <- row.optString("id"); + project <- row.optString("project") + ) yield (id, project) + } + .map(ls => Right(ls)) + .recover { case ex => + logger.error("Failed to insert feature", ex) + Left(InternalServerError()) + } + + } + + val wasmConfigs = features + .map { + case Feature(_, _, _, _, _, _, _, _) => None + case WasmFeature(_, _, _, _, wasmConfig, _, _, _) => Some(wasmConfig) + case s: SingleConditionFeature => None + } + .flatMap(o => o.toList) + + def unzip7[A: ClassTag, B: ClassTag, C: ClassTag, D: ClassTag, E: ClassTag, F: ClassTag, G: ClassTag]( + l: Iterable[(A, B, C, D, E, F, G)] + ): (Array[A], Array[B], Array[C], Array[D], Array[E], Array[F], Array[G]) = { + l.foldLeft(Tuple7(Array[A](), Array[B](), Array[C](), Array[D](), Array[E](), Array[F](), Array[G]())) { + case (res, (e1, e2, e3, e4, e5, e6, e7)) => + ( + res._1.appended(e1), + res._2.appended(e2), + res._3.appended(e3), + res._4.appended(e4), + res._5.appended(e5), + res._6.appended(e6), + res._7.appended(e7) + ) + } + } + + val (modernFeatures, wasmFeatures, legacyFeatures): ( + ArrayBuffer[Feature], + ArrayBuffer[WasmFeature], + ArrayBuffer[SingleConditionFeature] + ) = (ArrayBuffer(), ArrayBuffer(), ArrayBuffer()) + features.foreach { + case f @ Feature(_, _, _, _, _, _, _, _) => + modernFeatures.addOne(f) + case wf @ WasmFeature(_, _, _, _, _, _, _, _) => + wasmFeatures.addOne(wf) + case s: SingleConditionFeature => + legacyFeatures.addOne(s) + } + + val legacyFeatureParams = unzip7(legacyFeatures.map { + case SingleConditionFeature(id, name, project, conditions, enabled, tags, metadata, description) => + ( + Option(id).getOrElse(UUID.randomUUID().toString), + name, + project, + java.lang.Boolean.valueOf(enabled), + new JsonObject(Json.toJson(conditions).toString()), + metadata.vertxJsValue, + description + ) + }) + + val modernFeatureParams = unzip7( + modernFeatures.map { case Feature(id, name, project, conditions, enabled, tags, metadata, description) => + ( + Option(id).getOrElse(UUID.randomUUID().toString), + name, + project, + java.lang.Boolean.valueOf(enabled), + new JsonArray(Json.toJson(conditions).toString()), + metadata.vertxJsValue, + description + ) + } + ) + + val wasmFeatureParams = unzip7( + wasmFeatures.map { case WasmFeature(id, name, project, enabled, wasmConfig, tags, metadata, description) => + ( + Option(id).getOrElse(UUID.randomUUID().toString), + name, + project, + java.lang.Boolean.valueOf(enabled), + wasmConfig.name, + metadata.vertxJsValue, + description + ) + } + ) + + createWasmScripts(tenant, wasmConfigs.toList, conflictStrategy, conn.some) + .flatMap { + case Left(err) => Left(List(err)).future + case Right(_) => { + Future + .sequence( + List( + insertFeatures(modernFeatureParams), + insertFeatures(legacyFeatureParams), + env.postgresql + .queryAll( + s"""INSERT INTO features (id, name, project, enabled, script_config, metadata, description) + |VALUES (unnest($$1::TEXT[]), unnest($$2::TEXT[]), unnest($$3::TEXT[]), unnest($$4::BOOLEAN[]), unnest($$5::TEXT[]), unnest($$6::JSONB[]), unnest($$7::TEXT[])) + |returning id, project""".stripMargin, + wasmFeatureParams.productIterator.toList.map(a => a.asInstanceOf[AnyRef]), + conn = conn.some, + schemas = Set(tenant) + ) { row => + for ( + id <- row.optString("id"); + project <- row.optString("project") + ) yield (id, project) + } + .map(ls => Right(ls)) + .recover { + case f: PgException if f.getSqlState == RELATION_DOES_NOT_EXISTS => + Left(TenantDoesNotExists(tenant)) + case ex => + logger.error("Failed to insert feature", ex) + Left(InternalServerError()) + } + ) + ) + .map(eithers => eithers.toEitherList.map(l => l.flatten)) + } + } + .flatMap { + case Left(errors) => Future.successful(Left(errors)) + case Right(ids) => + Future + .sequence(features.map(f => insertIntoFeatureTags(tenant, f.id, f.tags, conn.some))) + .map(eithers => eithers.toEitherList.map(_ => ids)) + } + .flatMap { + case Left(errors) => Future.successful(Left(errors)) + case Right(ids) => { + Future + .sequence(ids.map { case (id, project) => + emitEvent(tenant, id = id, eventType = FeatureCreated, sourceProject = project, conn = conn) + }) + .map(_ => Right(())) + } + } + } + + def create(tenant: String, project: String, feature: AbstractFeature): Future[Either[IzanamiError, String]] = { + env.postgresql.executeInTransaction( + implicit conn => doCreate(tenant, project, feature, conn), + schemas = Set(tenant) + ) + } + + private def doCreate( + tenant: String, + project: String, + feature: AbstractFeature, + conn: SqlConnection + ): Future[Either[IzanamiError, String]] = { + (feature match { + case Feature(_, _, _, _, _, _, _, _) => Future(Right(())) + case WasmFeature(_, _, _, _, wasmConfig, _, _, _) => + createWasmScriptIfNeeded(tenant, wasmConfig, conn = Some(conn)) + case s: SingleConditionFeature => Future(Right(())) + }).flatMap { + case Left(err) => Left(err).future + case Right(_) => { + insertFeature(tenant, project, feature)(conn) + .flatMap(eitherId => { + eitherId.fold( + err => Future.successful(Left(err)), + id => insertIntoFeatureTags(tenant, id, feature.tags, Some(conn)).map(either => either.map(_ => id)) + ) + }) + } + } + } + + def readLocalScripts(tenant: String): Future[Seq[WasmConfig]] = { + env.postgresql.queryAll( + s""" + |SELECT config FROM wasm_script_configurations + |""".stripMargin, + List(), + schemas = Set(tenant) + ) { r => r.optJsObject("config").map(js => js.as(WasmConfig.format)) } + } + + def deleteLocalScript(tenant: String, name: String): Future[Either[IzanamiError, Unit]] = { + env.postgresql + .queryOne( + s""" + |DELETE FROM wasm_script_configurations WHERE id=$$1 + |""".stripMargin, + List(name), + schemas = Set(tenant) + ) { r => Some(()) } + .map(_ => Right(())) + .recover { + case f: PgException if f.getSqlState == FOREIGN_KEY_VIOLATION => Left(FeatureDependsOnThisScript()) + } + } + + def readLocalScriptsWithAssociatedFeatures(tenant: String): Future[Seq[WasmConfigWithFeatures]] = { + env.postgresql.queryAll( + s""" + |SELECT c.config, json_agg(json_build_object('id', features.id, 'name', features.name, 'project', features.project)) as features + |FROM wasm_script_configurations c + |LEFT JOIN features ON features.script_config=c.id + |GROUP BY c.config + |""".stripMargin, + List(), + schemas = Set(tenant) + ) { r => + { + r.optJsObject("config") + .map(js => js.as(WasmConfig.format)) + .flatMap(config => { + r.optJsArray("features") + .map(arr => { + val features = arr.value + .map(jsValue => { + for { + name <- (jsValue \ "name").asOpt[String] + id <- (jsValue \ "id").asOpt[String] + project <- (jsValue \ "project").asOpt[String] + } yield WasmScriptAssociatedFeatures(name = name, project = project, id = id) + }) + .filter(o => o.isDefined) + .map(o => o.get) + .toSeq + + WasmConfigWithFeatures(wasmConfig = config, features = features) + }) + }) + } + } + } + + def readAllLocalScripts(): Future[Seq[WasmConfig]] = { + env.datastores.tenants + .readTenants() + .flatMap(tenants => { + Future.sequence(tenants.map(tenant => { + env.postgresql.queryOne( + s""" + |SELECT config FROM "${tenant.name}".wasm_script_configurations + |""".stripMargin, + List() + ) { r => r.optJsObject("config").map(js => js.as(WasmConfig.format)) } + })) + }) + .map(os => os.filter(o => o.isDefined).map(o => o.get)) + } + + def createWasmScriptIfNeeded( + tenant: String, + wasmConfig: WasmConfig, + conn: Option[SqlConnection] + ): Future[Either[IzanamiError, String]] = { + wasmConfig.source.kind match { + case WasmSourceKind.Unknown => throw new RuntimeException("Unknown wasm script") + case WasmSourceKind.Local => Right(wasmConfig.source.path).future + case _ => + env.postgresql + .queryOne( + s"""INSERT INTO wasm_script_configurations (id, config) VALUES ($$1,$$2) RETURNING id""", + List(wasmConfig.name, Json.toJson(wasmConfig)(WasmConfig.format).vertxJsValue), + conn = conn, + schemas = Set(tenant) + ) { row => row.optString("id") } + .map(o => o.toRight(InternalServerError())) + .recover { + case f: PgException if f.getSqlState == FOREIGN_KEY_VIOLATION => + Left(WasmScriptAlreadyExists(wasmConfig.source.path)) + } + .flatMap(either => { + // TODO this should be elsewhere + wasmConfig.source.getWasm()(env.wasmIntegration.context, env.executionContext).map(_ => either) + }) + } + } + + def createWasmScripts( + tenant: String, + wasmConfigs: List[WasmConfig], + conflictStrategy: ImportConflictStrategy, + conn: Option[SqlConnection] + ): Future[Either[IzanamiError, Set[String]]] = { + + if (wasmConfigs.isEmpty) { + Future.successful(Right(Set())) + } else { + + val (ids, scripts) = wasmConfigs + .filter(w => w.source.kind != WasmSourceKind.Local && w.source.kind != WasmSourceKind.Unknown) + .map(w => (w.name, Json.toJson(w)(WasmConfig.format).vertxJsValue)) + .unzip + + val localScriptIds = wasmConfigs.filter(w => w.source.kind == WasmSourceKind.Local).map(w => w.name) + + env.postgresql + .queryRaw( + s""" + |INSERT INTO wasm_script_configurations(id, config) + |VALUES (unnest($$1::TEXT[]), unnest($$2::JSONB[])) + |${conflictStrategy match { + case Fail => "" + case MergeOverwrite => + """ + |ON CONFLICT(id) DO UPDATE SET config = excluded.config + |""".stripMargin + case Skip => " ON CONFLICT(id) DO NOTHING " + }} + |returning id + |""".stripMargin, + List(ids.toArray, scripts.toArray), + schemas = Set(tenant), + conn = conn + ) { rs => rs.flatMap(_.optString("id")).toSet } + .map(ids => { + ids.foreach(id => + wasmConfigs.find(w => w.name == id).get.source.getWasm()(env.wasmIntegration.context, env.executionContext) + ) + Right(ids.concat(localScriptIds)) + }) + .recover { + case f: PgException if f.getSqlState == FOREIGN_KEY_VIOLATION => + Left(WasmScriptAlreadyExists("")) // TODO specify script name + } + } + } + + def updateWasmScript( + tenant: String, + script: String, + wasmConfig: WasmConfig + ): Future[Unit] = { + env.postgresql + .queryOne( + s"""UPDATE wasm_script_configurations SET id=$$1, config=$$2 WHERE id=$$3 RETURNING id""", + List( + wasmConfig.name, + wasmConfig.json.vertxJsValue, + script + ), + schemas = Set(tenant) + ) { row => row.optString("id") } + .map(o => ()) + } + + private def insertFeature( + tenant: String, + project: String, + feature: AbstractFeature, + importConflictStrategy: ImportConflictStrategy = Fail + )(implicit + conn: SqlConnection + ): Future[Either[IzanamiError, String]] = { + val (request, params) = feature match { + case SingleConditionFeature(id, name, project, conditions, enabled, _, metadata, description) => + ( + s"""INSERT INTO features (id, name, project, enabled, conditions, metadata, description) + |VALUES ($$1, $$2, $$3, $$4, $$5, $$6, $$7) + |returning id""".stripMargin, + List( + Option(id).getOrElse(UUID.randomUUID().toString), + name, + project, + java.lang.Boolean.valueOf(enabled), + new JsonObject(Json.toJson(conditions).toString()), + metadata.vertxJsValue, + description + ) + ) + case Feature(id, name, project, conditions, enabled, _, metadata, description) => + ( + s"""INSERT INTO features (id, name, project, enabled, conditions, metadata, description) + |VALUES ($$1, $$2, $$3, $$4, $$5, $$6, $$7) + |returning id""".stripMargin, + List( + Option(id).getOrElse(UUID.randomUUID().toString), + name, + project, + java.lang.Boolean.valueOf(enabled), + new JsonArray(Json.toJson(conditions).toString()), + metadata.vertxJsValue, + description + ) + ) + case WasmFeature(id, name, project, enabled, config, _, metadata, description) => + ( + s"""INSERT INTO features (id, name, project, enabled, script_config, metadata, description) + |VALUES ($$1, $$2, $$3, $$4, $$5, $$6, $$7) + |returning id""".stripMargin, + List( + Option(id).getOrElse(UUID.randomUUID().toString), + name, + project, + java.lang.Boolean.valueOf(enabled), + config.name, + metadata.vertxJsValue, + description + ) + ) + } + + env.postgresql + .queryOne( + request, + params, + conn = Some(conn), + schemas = Set(tenant) + ) { row => row.optString("id") } + .map(_.toRight(InternalServerError())) + .recover { + case f: PgException if f.getSqlState == FOREIGN_KEY_VIOLATION => Left(ProjectDoesNotExists(project)) + case f: PgException if f.getSqlState == RELATION_DOES_NOT_EXISTS => Left(TenantDoesNotExists(tenant)) + case ex => + logger.error("Failed to insert feature", ex) + Left(InternalServerError()) + } + .flatMap { + case Left(error) => Future.successful(Left(error)) + case Right(id) => + emitEvent(tenant, id = id, eventType = FeatureCreated, sourceProject = project, conn = conn).map(_ => + Right(id) + ) + } + } + + def update(tenant: String, id: String, feature: AbstractFeature): Future[Either[IzanamiError, String]] = { + // TODO allow updating metadata + env.postgresql.executeInTransaction( + conn => { + val (request, params) = feature match { + case SingleConditionFeature(id, name, project, conditions, enabled, tags, metadata, description) => + ( + s"""update features + |SET name=$$1, enabled=$$2, conditions=$$3, script_config=NULL, description=$$5, project=$$6 WHERE id=$$4 returning id""".stripMargin, + List( + name, + java.lang.Boolean.valueOf(enabled), + new JsonObject(Json.toJson(conditions).toString()), + id, + description, + project + ) + ) + case Feature(_, name, project, conditions, enabled, _, _, description) => + ( + s"""update features + |SET name=$$1, enabled=$$2, conditions=$$3, script_config=NULL, description=$$5, project=$$6 WHERE id=$$4 returning id""".stripMargin, + List( + name, + java.lang.Boolean.valueOf(enabled), + new JsonArray(Json.toJson(conditions).toString()), + id, + description, + project + ) + ) + case WasmFeature(_, name, project, enabled, wasmConfig, _, _, description) => + ( + s"""update features + |SET name=$$1, enabled=$$2, script_config=$$4, conditions=NULL, description=$$5, project=$$6 WHERE id=$$3 returning id""".stripMargin, + List( + name, + java.lang.Boolean.valueOf(enabled), + id, + wasmConfig.name, + description, + project + ) + ) + } + + (feature match { + case feat @ WasmFeature(_, _, _, _, wasmConfig, _, _, _) if wasmConfig.source.kind != WasmSourceKind.Local => + createWasmScriptIfNeeded(tenant, wasmConfig, Some(conn)) + case _ => Future(()) + }) + .flatMap(_ => + env.postgresql.queryRaw( + s""" + |DELETE FROM feature_contexts_strategies fc USING features f + |WHERE fc.feature=f.name + |AND fc.project=f.project + |AND f.id=$$1 + |AND f.project != $$2 + |AND fc.local_context IS NOT NULL + |""".stripMargin, + List(id, feature.project), + conn = Some(conn) + ) { _ => Some(()) } + ) + .flatMap(_ => + env.postgresql + .queryOne( + request, + params, + conn = Some(conn) + ) { row => row.optString("id") } + .map(maybeId => maybeId.toRight(InternalServerError())) + .recover { + case f: PgException if f.getSqlState == NOT_NULL_VIOLATION => Left(MissingFeatureFields()) + case _ => Left(InternalServerError()) + } + .flatMap(either => { + either.fold( + err => Future.successful(Left(err)), + id => { + env.postgresql + .queryOne( + s"""delete from features_tags where feature=$$1""", + List(id), + conn = Some(conn) + ) { _ => Some(id) } + .flatMap(_ => + insertIntoFeatureTags(tenant, id, feature.tags, Some(conn)).map(either => either.map(_ => id)) + ) + } + ) + }) + ) + .flatMap { + case l @ Left(_) => Future.successful(l) + case r @ Right(id) => + emitEvent( + tenant, + id = id, + eventType = FeatureUpdated, + sourceProject = feature.project, + conn = conn + ).map(_ => r) + } + }, + schemas = Set(tenant) + ) + } + + def insertIntoFeatureTags( + tenant: String, + id: String, + tags: Set[String], + conn: Option[SqlConnection] + ): Future[Either[IzanamiError, Unit]] = { + if (tags.isEmpty) { + Future.successful(Right(())) + } else { + env.postgresql + .queryOne( + s""" + |INSERT INTO features_tags (feature, tag) + |VALUES ($$1, unnest($$2::TEXT[])) returning *""".stripMargin, + List(id, tags.toArray), + conn = conn, + schemas = Set(tenant) + ) { _ => Some(()) } + .map { _.toRight(InternalServerError()) } + .recover { + case f: PgException if f.getSqlState == RELATION_DOES_NOT_EXISTS => Left(TenantDoesNotExists(tenant)) + case f: PgException if f.getSqlState == FOREIGN_KEY_VIOLATION => + Left(TagDoesNotExists(tags.map(t => t).mkString(","))) + case ex => + logger.error("Failed to update feature/tag mapping table", ex) + Left(InternalServerError()) + } + } + } + + def delete(tenant: String, id: String): Future[Either[IzanamiError, String]] = { + env.postgresql.executeInTransaction(conn => + env.postgresql + .queryOne( + s"""DELETE FROM features WHERE id=$$1 returning id, project""", + List(id), + schemas = Set(tenant), + conn = Some(conn) + ) { row => + for ( + id <- row.optString("id"); + project <- row.optString("project") + ) yield (id, project) + } + .map { _.toRight(InternalServerError()) } + .recover { + case ex: PgException if ex.getSqlState == RELATION_DOES_NOT_EXISTS => Left(TenantDoesNotExists(tenant)) + case _ => Left(InternalServerError()) + } + .flatMap { + case l @ Left(err) => Future.successful(Left(err)) + case Right((id, project)) => + emitEvent(tenant, id = id, eventType = FeatureDeleted, sourceProject = project, conn = conn).map(_ => + Right(id) + ) + } + ) + } +} + +object featureImplicits { + implicit class FeatureRow(val row: Row) extends AnyVal { + + def optFeature(): Option[AbstractFeature] = { + val tags = + row.optJsArray("tags").map(array => array.value.map(v => v.as[String]).toSet).getOrElse(Set()) + + val maybeClassicalConditions = row + .optJsArray("contextual_conditions") + .orElse(row.optJsArray("conditions")) + .map(arr => arr.value.map(v => v.as[ActivationCondition]).toSet) + + lazy val maybeLegacyConditions = row + .optJsObject("contextual_conditions") + .orElse(row.optJsObject("conditions")) + .map(v => v.as[LegacyCompatibleCondition]) + + lazy val maybeWasmConfig = row + .optJsObject("contextual_wasm") + .orElse(row.optJsObject("wasm")) + .map(jsObject => jsObject.as[WasmConfig](WasmConfig.format)) + + for ( + name <- row.optString("name"); + id <- row.optString("id"); + description <- row.optString("description"); + project <- row.optString("project"); + enabled <- row.optBoolean("contextual_enabled").orElse(row.optBoolean("enabled")); + metadata <- row.optJsObject("metadata") + ) + yield (maybeClassicalConditions, maybeLegacyConditions, maybeWasmConfig) match { + case (Some(classicalConditions), _, _) => { + Feature( + id = id, + name = name, + project = project, + enabled = enabled, + conditions = classicalConditions, + metadata = metadata, + tags = tags, + description = description + ) + } + case (_, Some(legacyCompatibleCondition), _) => { + SingleConditionFeature( + id = id, + name = name, + project = project, + enabled = enabled, + condition = legacyCompatibleCondition, + metadata = metadata, + tags = tags, + description = description + ) + } + case (_, _, Some(wasmConfig)) => { + WasmFeature( + id = id, + name = name, + project = project, + enabled = enabled, + wasmConfig = wasmConfig, + metadata = metadata, + tags = tags, + description = description + ) + } + case _ => throw new RuntimeException("Failed to read feature " + id) + } + } + } +} diff --git a/app/fr/maif/izanami/datastores/HashUtils.scala b/app/fr/maif/izanami/datastores/HashUtils.scala new file mode 100644 index 000000000..0e628d01d --- /dev/null +++ b/app/fr/maif/izanami/datastores/HashUtils.scala @@ -0,0 +1,24 @@ +package fr.maif.izanami.datastores + +import org.apache.commons.codec.binary.Hex +import org.mindrot.jbcrypt.BCrypt._ + +import java.security.MessageDigest + +object HashUtils { + def bcryptHash(input: String): String = { + hashpw(input, gensalt()) + } + + def bcryptCheck(input: String, hashed: String): Boolean = { + checkpw(input, hashed) + } + + def sha512(toHash: String): Array[Byte] = + MessageDigest.getInstance("SHA-512").digest(toHash.getBytes) + + def hexSha512(toHash: String): String = + Hex encodeHexString MessageDigest + .getInstance("SHA-512") + .digest(toHash.getBytes) +} diff --git a/app/fr/maif/izanami/datastores/ProjectsDatastore.scala b/app/fr/maif/izanami/datastores/ProjectsDatastore.scala new file mode 100644 index 000000000..fdd9d1fa5 --- /dev/null +++ b/app/fr/maif/izanami/datastores/ProjectsDatastore.scala @@ -0,0 +1,259 @@ +package fr.maif.izanami.datastores + +import fr.maif.izanami.datastores.projectImplicits.ProjectRow +import fr.maif.izanami.env.Env +import fr.maif.izanami.env.PostgresqlErrors.{RELATION_DOES_NOT_EXISTS, UNIQUE_VIOLATION} +import fr.maif.izanami.env.pgimplicits.EnhancedRow +import fr.maif.izanami.errors._ +import fr.maif.izanami.models.{Feature, Project, ProjectCreationRequest, RightLevels} +import fr.maif.izanami.utils.Datastore +import fr.maif.izanami.utils.syntax.implicits.BetterSyntax +import fr.maif.izanami.web.ImportController +import fr.maif.izanami.web.ImportController.{Fail, ImportConflictStrategy, MergeOverwrite, Skip} +import io.vertx.pgclient.PgException +import io.vertx.sqlclient.{Row, SqlConnection} + +import java.util.regex.Pattern +import scala.concurrent.Future + +class ProjectsDatastore(val env: Env) extends Datastore { + + def createProjects( + tenant: String, + names: Set[String], + conflictStrategy: ImportConflictStrategy, + user: String, + conn: Option[SqlConnection] = None + ): Future[Either[IzanamiError, Unit]] = { + env.postgresql.queryRaw( + s""" + |INSERT INTO projects(name, description) VALUES(unnest($$1::text[]), '') + |${ + conflictStrategy match { + case Fail => "" + case MergeOverwrite => """ + | ON CONFLICT(name) DO UPDATE SET description = excluded.description + |""".stripMargin + case Skip => " ON CONFLICT(name) DO NOTHING " + }} + |RETURNING name + |""".stripMargin, + List(names.toArray), + schemas = Set(tenant), + conn=conn + ){_ => Some(())} + .map(o => { + o.toRight(InternalServerError()) + }) + .flatMap { + case Left(err) => Left(err).future + case Right(value) => env.postgresql.queryRaw( + s"""INSERT INTO users_projects_rights (username, project, level) + |VALUES ($$1, unnest($$2::TEXT[]), $$3) + |ON CONFLICT(username, project) DO NOTHING + |RETURNING 1 + |""".stripMargin, + List(user, names.toArray, RightLevels.Admin.toString.toUpperCase), + conn = conn + ) { _ => Some(value) } + .map(o => { + o.toRight(InternalServerError()) + }) + } + .recover{ + case f: PgException if f.getSqlState == UNIQUE_VIOLATION => { + val regexp = Pattern.compile("Key \\(name\\)=\\((?.*)\\) already exists\\.") + val matcher = regexp.matcher(f.getDetail) + matcher.matches() + val name = matcher.group("name") + Left(ProjectAlreadyExists(if(name.isEmpty) "" else name, tenant)) + } + case ex => + logger.error("Failed to update project mapping table", ex) + Left(InternalServerError()) + } + } + + def createProject( + tenant: String, + projectCreationRequest: ProjectCreationRequest, + user: String + ): Future[Either[IzanamiError, Project]] = { + env.postgresql.executeInTransaction(conn => { + env.postgresql + .queryOne( + s"""insert into projects (name, description) values ($$1, $$2) returning *""", + List(projectCreationRequest.name, projectCreationRequest.description), + conn = Some(conn) + ) { row => row.optProject() } + .map { + _.toRight(InternalServerError()) + } + .recover { + case f: PgException if f.getSqlState == UNIQUE_VIOLATION => + Left(ProjectAlreadyExists(projectCreationRequest.name, tenant)) + case f: PgException if f.getSqlState == RELATION_DOES_NOT_EXISTS => Left(TenantDoesNotExists(tenant)) + case ex => + logger.error("Failed to insert project", ex) + Left(InternalServerError()) + }.flatMap { + case Left(value) => Left(value).future + case Right(value) => env.postgresql.queryOne( + s"""INSERT INTO users_projects_rights (username, project, level) VALUES ($$1, $$2, $$3) RETURNING project""", + List(user, projectCreationRequest.name, RightLevels.Admin.toString.toUpperCase), + conn = Some(conn) + ){_ => Some(value)} + .map (_.toRight(InternalServerError())) + } + },schemas = Set(tenant)) + } + + def updateProject(tenant: String, oldName: String, newProject: ProjectCreationRequest): Future[Unit] = { + env.postgresql.queryOne( + s""" + |UPDATE projects SET name=$$1, description=$$2 WHERE name=$$3 RETURNING * + |""".stripMargin, + List(newProject.name, newProject.description, oldName), + schemas = Set(tenant) + ){_ => Some(())} + .map(_ => ()) + } + + + def readTenantProjectForUser(tenant: String, user: String): Future[List[Project]] = { + // TODO ensure performance of this query + env.postgresql.queryAll(s""" + SELECT p.* + FROM projects p + WHERE EXISTS (SELECT u.username FROM izanami.users u WHERE u.username=$$1 AND u.admin=TRUE) + OR EXISTS(SELECT * FROM izanami.users_tenants_rights utr WHERE utr.username=$$1 AND (utr.level='ADMIN')) + OR p.name=ANY(SELECT upr.project FROM users_projects_rights upr WHERE upr.username=$$1) + """, List(user), schemas = Set(tenant)) { row => row.optProject() } + } + + def deleteProject(tenant: String, project: String): Future[Either[IzanamiError, List[String]]] = { + env.postgresql.executeInTransaction(conn => { + env.postgresql + .queryOne(s"""DELETE FROM projects p WHERE p.name=$$1 RETURNING (SELECT array_agg(f.id) AS ids FROM features f WHERE f.project=p.name);""", + List(project), + schemas = Set(tenant), + conn=Some(conn) + ) { row => row.optStringArray("ids").map(_.toList).orElse(Some(List())) } + .map(o => o.toRight(ProjectDoesNotExists(project))) + .flatMap { + case Left(value) => Future.successful(Left(value)) + case Right(ids) => Future.sequence(ids.map(id => env.datastores.features.emitEvent(tenant, id, FeatureDeleted, sourceProject=project, conn=conn))).map(_ => Right(ids)) + } + }) + + } + + def readProject(tenant: String, project: String): Future[Either[IzanamiError, Project]] = { + env.postgresql + .queryOne( + s""" + |select p.id, p.name, p.description, + | COALESCE( + | json_agg(row_to_json(f.*)::jsonb + | || (json_build_object('tags', ( + | array( + | SELECT ft.tag + | FROM features_tags ft + | WHERE ft.feature = f.id + | GROUP BY ft.tag + | ) + | ), 'wasmConfig', ( + | select w.config FROM wasm_script_configurations w where w.id = f.script_config + | )))::jsonb) + | FILTER (WHERE f.id IS NOT NULL), '[]' + | ) as "features" + |from projects p + |left join features f on p.name = f.project + |WHERE p.name = $$1 + |group by p.id""".stripMargin, + List(project), + schemas = Set(tenant) + ) { row => row.optProjectWithFeatures() } + .map(o => o.toRight(ProjectDoesNotExists(project))) + } + + def readProjects(tenant: String): Future[List[Project]] = { + env.postgresql + .queryAll( + s"""select p.id, p.name, p.description, + | COALESCE( + | json_agg(row_to_json(f.*)::jsonb + | || (json_build_object('tags', ( + | array( + | SELECT ft.tag + | FROM features_tags ft + | WHERE ft.feature = f.id + | GROUP BY ft.tag + | ) + | ),'wasmConfig', ( + | select w.config FROM wasm_script_configurations w where w.id = f.script_config + | )))::jsonb) + | FILTER (WHERE f.id IS NOT NULL), '[]' + | ) as "features" + |from projects p + |left join features f on p.name = f.project + |group by p.id""".stripMargin, + List(), + schemas = Set(tenant) + ) { row => row.optProjectWithFeatures() } + } + + def readProjectsFiltered(tenant: String, projectFilter: Set[String]): Future[List[Project]] = { + env.postgresql + .queryAll( + s"""select p.id, p.name, p.description, + | COALESCE( + | json_agg(row_to_json(f.*)::jsonb + | || (json_build_object('tags', ( + | array( + | SELECT ft.tag + | FROM features_tags ft + | WHERE ft.feature = f.id + | GROUP BY ft.tag + | ) + | ), 'wasmConfig', ( + | select w.config FROM wasm_script_configurations w where w.id = f.script_config + | )))::jsonb) + | FILTER (WHERE f.id IS NOT NULL), '[]' + | ) as "features" + |from projects p + |left join features f on p.name = f.project + |where p.name=ANY($$1) + |group by p.id""".stripMargin, + List(projectFilter.toArray), + schemas = Set(tenant) + ) { row => row.optProjectWithFeatures() } + } +} + +object projectImplicits { + implicit class ProjectRow(val row: Row) extends AnyVal { + def optProject(): Option[Project] = { + for ( + id <- row.optUUID("id"); + name <- row.optString("name"); + description <- row.optString("description") + ) + yield Project(id=id, name = name, features = List(), description = description) + } + + def optProjectWithFeatures(): Option[Project] = { + for ( + id <- row.optUUID("id"); + name <- row.optString("name"); + description <- row.optString("description") + ) + yield { + val maybeFeatures = row + .optJsArray("features") + .map(array => array.value.map(v => Feature.readFeature(v, name).asOpt).flatMap(o => o.toList).toList) + Project(id=id, name = name, features = maybeFeatures.getOrElse(List()), description = description) + } + } + } +} diff --git a/app/fr/maif/izanami/datastores/StatsDatastore.scala b/app/fr/maif/izanami/datastores/StatsDatastore.scala new file mode 100644 index 000000000..ecf757555 --- /dev/null +++ b/app/fr/maif/izanami/datastores/StatsDatastore.scala @@ -0,0 +1,243 @@ +package fr.maif.izanami.datastores + +import akka.actor.Cancellable +import buildinfo.BuildInfo +import fr.maif.izanami.env.Env +import fr.maif.izanami.env.pgimplicits.EnhancedRow +import fr.maif.izanami.models.IzanamiConfiguration +import fr.maif.izanami.security.IdGenerator +import fr.maif.izanami.utils.Datastore +import io.vertx.sqlclient.{Row, SqlConnection} +import play.api.libs.json.{JsNumber, JsObject, JsValue, Json} + +import java.time.{Instant, ZoneId} +import java.time.format.DateTimeFormatter +import scala.concurrent.Future +import scala.concurrent.duration.DurationInt + +class StatsDatastore(val env: Env) extends Datastore { + var anonymousReportingCancellation: Cancellable = Cancellable.alreadyCancelled + + override def onStart(): Future[Unit] = { + anonymousReportingCancellation = env.actorSystem.scheduler.scheduleAtFixedRate(0.minutes, 24.hours)(() => + env.datastores.configuration.readConfiguration().foreach { + case Right(conf) if conf.anonymousReporting => { + sendAnonymousReporting() + } + case _ => + } + ) + Future.successful(()) + } + + + override def onStop(): Future[Unit] = { + anonymousReportingCancellation.cancel() + Future.successful(()) + } + + def sendAnonymousReporting(): Future[Unit] = { + retrieveStats().flatMap(json => { + env.Ws.url(env.configuration.get[String]("app.reporting.url")).post(json) + }).map(_ => ()) + } + + def retrieveStats(): Future[JsValue] = { + env.postgresql.executeInTransaction(conn => { + env.postgresql + .queryAll(s""" + |SELECT name FROM izanami.tenants + |""".stripMargin) { r => r.optString("name") } + .flatMap(names => + Future.sequence(names.map(name => { + retrieveTenantStats(name, conn) + }))).map(l => l.foldLeft(TenantStats())((s1, s2) => s1.mergeWith(s2))) + }).flatMap(stats => { + retrieveRunInformations().map(runInfo => runInfo ++ Json.obj("entities" -> stats.toJson)) + }).flatMap(json => { + readMailerType().map(mailerInfo => { + val features = mailerInfo ++ readIntegrationInformations() + json ++ Json.obj("features" -> features) + }) + }).map(json => { + json ++ Json.obj("stats" -> Json.obj(), "tenants" -> Json.arr(), "containerized" -> isContainerized) + }) + } + + def isContainerized: Boolean = env.configuration.get[Boolean]("app.containerized") + + def retrieveRunInformations(): Future[JsObject] = { + val now = Instant.now() + for( + izanamiId <- env.datastores.configuration.readId() + ) yield Json.obj( + "os" -> Json.obj("name" -> System.getProperty("os.name"), "arch" -> System.getProperty("os.arch"), "version" -> System.getProperty("os.version")), + "izanami_version" -> BuildInfo.version, + "java_version" -> Json.obj( + "version" -> System.getProperty("java.version"), + "vendor" -> System.getProperty("java.vendor") + ), + "@id" -> IdGenerator.uuid, + "izanami_cluster_id" -> izanamiId, + "@timestamp" -> now.toEpochMilli, + "timestamp_str" -> now.atZone(ZoneId.systemDefault()).format(DateTimeFormatter.ISO_OFFSET_DATE_TIME) + ) + } + + case class TenantStats( + classicalFeaturesCount: Int=0, + scriptFeaturesCount: Int=0, + classicalOverloadCount: Int=0, + scriptOverloadCount: Int=0, + projectCount: Int=0, + adminKeyCount: Int=0, + nonAdminKeyCount: Int=0, + adminUserCount: Int=0, + nonAdminUserCount: Int=0, + tagCount: Int=0 + ) { + def mergeWith(other: TenantStats): TenantStats = { + copy( + classicalFeaturesCount=classicalFeaturesCount + other.classicalFeaturesCount, + scriptFeaturesCount=scriptFeaturesCount+other.scriptFeaturesCount, + classicalOverloadCount=classicalOverloadCount+other.classicalOverloadCount, + scriptOverloadCount=scriptOverloadCount+other.scriptOverloadCount, + projectCount=projectCount+other.projectCount, + adminKeyCount=adminKeyCount+other.adminKeyCount, + nonAdminKeyCount=nonAdminKeyCount+other.nonAdminKeyCount, + adminUserCount=adminUserCount+other.adminUserCount, + nonAdminUserCount=nonAdminUserCount+other.nonAdminUserCount, + tagCount=tagCount+other.tagCount + ) + } + + def toJson: JsObject = { + Json.obj( + "classicalFeaturesCount" -> classicalFeaturesCount, + "scriptFeaturesCount" -> scriptFeaturesCount, + "classicalOverloadCount" -> classicalOverloadCount, + "scriptOverloadCount" -> scriptOverloadCount, + "projectCount" -> projectCount, + "adminKeyCount" -> adminKeyCount, + "nonAdminKeyCount" -> nonAdminKeyCount, + "adminUserCount" -> adminUserCount, + "nonAdminUserCount" -> nonAdminUserCount, + "tagCount" -> tagCount + ) + } + } + + def retrieveTenantStats(tenant: String, conn: SqlConnection): Future[TenantStats] = { + env.postgresql + .queryRaw( + s""" + |select count(id), script_config is null as classical from features group by classical + |""".stripMargin, + schemas = Set(tenant), + conn=Some(conn) + ) { rows => + { + readBooleanCount(rows, "classical") + } + } + .flatMap { + case (classicalFeaturesCount, scriptFeaturesCount) => { + env.postgresql + .queryRaw( + s""" + |select count(*), script_config is null as classical from feature_contexts_strategies group by classical + |""".stripMargin, + schemas = Set(tenant), + conn=Some(conn) + ) { rows => + { + readBooleanCount(rows, "classical") + } + } + .map { case (classicalOverloadCount, scriptOverloadCount) => + TenantStats( + classicalFeaturesCount=classicalFeaturesCount, + scriptFeaturesCount=scriptFeaturesCount, + classicalOverloadCount=classicalOverloadCount, + scriptOverloadCount=scriptOverloadCount + ) + } + } + } + .flatMap(stats => { + env.postgresql + .queryOne( + s""" + |select count(*) from projects + |""".stripMargin, + schemas = Set(tenant), + conn=Some(conn) + ) { r => r.optInt("count") } + .map(o => o.getOrElse(0)) + .map(projectCount => stats.copy(projectCount=projectCount)) + }) + .flatMap(stats => { + env.postgresql + .queryRaw( + s""" + |select count(*), admin from apikeys group by admin + |""".stripMargin, + schemas = Set(tenant), + conn=Some(conn) + ) { rows => readBooleanCount(rows, "admin") } + .map { case (admin, nonAdmin) => stats.copy(adminKeyCount=admin, nonAdminKeyCount=nonAdmin) } + }) + .flatMap(stats => { + env.postgresql + .queryRaw( + s""" + |select count(*), admin from izanami.users group by admin + |""".stripMargin, + conn=Some(conn) + ) { rows => readBooleanCount(rows, "admin") } + .map { case (admin, nonAdmin) => stats.copy(adminUserCount=admin, nonAdminUserCount=nonAdmin) } + }).flatMap(stats => { + env.postgresql + .queryOne( + s""" + |select count(*) from tags + |""".stripMargin, + schemas = Set(tenant), + conn=Some(conn) + ) { r => r.optInt("count") } + .map(o => o.getOrElse(0)) + .map(projectCount => stats.copy(projectCount=projectCount)) + }) + } + + private def readBooleanCount(rows: List[Row], booleanColumnName: String): (Int, Int) = { + rows + .map(r => for (count <- r.optInt("count"); classical <- r.optBoolean(booleanColumnName)) yield (count, classical)) + .filter(_.isDefined) + .map(_.get) + .foldLeft((0, 0): (Int, Int)) { + case ((_, second), (count, true)) => (count, second) + case ((first, _), (count, false)) => (first, count) + } + } + + def readIntegrationInformations(): JsObject = { + val isWasmPresent = env.datastores.configuration.readWasmConfiguration().isDefined + val isOidcPresent = env.datastores.configuration.readOIDCConfiguration().isDefined + + Json.obj( + "wasmo" -> isWasmPresent, + "oidc" -> isOidcPresent + ) + } + + def readMailerType(): Future[JsObject] = { + env.datastores.configuration.readConfiguration().map { + case Left(err) => Json.obj() + case Right(IzanamiConfiguration(mailer, invitationMode, _, _, _)) => Json.obj( + "mailer" -> mailer.toString, + "invitation_mode" -> invitationMode.toString + ) + } + } +} diff --git a/app/fr/maif/izanami/datastores/TagsDatastore.scala b/app/fr/maif/izanami/datastores/TagsDatastore.scala new file mode 100644 index 000000000..bcc5a2741 --- /dev/null +++ b/app/fr/maif/izanami/datastores/TagsDatastore.scala @@ -0,0 +1,85 @@ +package fr.maif.izanami.datastores + +import fr.maif.izanami.datastores.tagImplicits.TagRow +import fr.maif.izanami.env.Env +import fr.maif.izanami.env.pgimplicits.EnhancedRow +import fr.maif.izanami.errors.{InternalServerError, IzanamiError, TagDoesNotExists} +import fr.maif.izanami.models.{Tag, TagCreationRequest} +import fr.maif.izanami.utils.Datastore +import io.vertx.sqlclient.Row + +import scala.concurrent.Future + +class TagsDatastore(val env: Env) extends Datastore { + def createTag(tagCreationRequest: TagCreationRequest, tenant: String): Future[Either[IzanamiError, Tag]] = { + env.postgresql + .queryOne( + s"""insert into tags (name, description) values ($$1, $$2) returning *""", + List(tagCreationRequest.name, tagCreationRequest.description), + schemas=Set(tenant) + ) { row => row.optTag() } + .map { + _.toRight(InternalServerError()) + } + .recover { case ex => + logger.error("Failed to insert tag", ex) + Left(InternalServerError()) + } + } + + def createTags(tags: List[TagCreationRequest], tenant: String): Future[List[Tag]] = { + env.postgresql + .queryAll( + s"""insert into tags (name, description) values (unnest($$1::text[]), unnest($$2::text[])) returning *""", + List(tags.map(_.name).toArray, tags.map(_.description).toArray), + schemas=Set(tenant) + ) { row => row.optTag() } + } + + def readTag(tenant: String, name: String): Future[Either[IzanamiError, Tag]] = { + env.postgresql + .queryOne( + s"""SELECT * FROM tags WHERE name=$$1""", + List(name), + schemas=Set(tenant) + ) { row => row.optTag() } + .map { _.toRight(TagDoesNotExists(name)) } + } + + def deleteTag(tenant: String, name: String): Future[Either[IzanamiError, Unit]] = { + env.postgresql + .queryOne( + s"""DELETE FROM tags WHERE name=$$1 returning name, id""", + List(name), + schemas=Set(tenant) + ) { row => row.optTag() } + .map { _.toRight(TagDoesNotExists(name)).map(_ => ()) } + } + + def readTags(tenant: String, names: Set[String]): Future[List[Tag]] = { + env.postgresql + .queryAll( + s"""SELECT * FROM tags WHERE name=ANY($$1)""", + List(names.toArray), + schemas=Set(tenant) + ) { row => row.optTag() } + } + + def readTags(tenant: String): Future[List[Tag]] = { + env.postgresql.queryAll( + s"""SELECT * FROM tags""", + schemas=Set(tenant) + ) { row => row.optTag() } + } +} + +object tagImplicits { + implicit class TagRow(val row: Row) extends AnyVal { + def optTag(): Option[Tag] = { + for ( + name <- row.optString("name"); + id <- row.optUUID("id") + ) yield Tag(id=id, name = name, description = row.optString("description").orNull) + } + } +} diff --git a/app/fr/maif/izanami/datastores/TenantsDatastore.scala b/app/fr/maif/izanami/datastores/TenantsDatastore.scala new file mode 100644 index 000000000..d1e041f4a --- /dev/null +++ b/app/fr/maif/izanami/datastores/TenantsDatastore.scala @@ -0,0 +1,202 @@ +package fr.maif.izanami.datastores + +import com.zaxxer.hikari.{HikariConfig, HikariDataSource} +import fr.maif.izanami.datastores.tenantImplicits.TenantRow +import fr.maif.izanami.env.Env +import fr.maif.izanami.env.PostgresqlErrors.UNIQUE_VIOLATION +import fr.maif.izanami.env.pgimplicits.EnhancedRow +import fr.maif.izanami.errors.{InternalServerError, IzanamiError, TenantAlreadyExists, TenantDoesNotExists} +import fr.maif.izanami.models.{RightLevels, Tenant, TenantCreationRequest} +import fr.maif.izanami.utils.Datastore +import fr.maif.izanami.utils.syntax.implicits.{BetterJsValue, BetterSyntax} +import fr.maif.izanami.web.ImportState.{importFailureWrites, importResultReads, importSuccessWrites} +import fr.maif.izanami.web.{ImportFailure, ImportPending, ImportState, ImportSuccess} +import io.vertx.pgclient.PgException +import io.vertx.sqlclient.Row +import org.flywaydb.core.Flyway +import play.api.libs.json.Json + +import java.util.UUID +import scala.concurrent.Future + +class TenantsDatastore(val env: Env) extends Datastore { + def deleteImportStatus(id: UUID): Future[Unit] = { + env.postgresql.queryOne( + s""" + |DELETE FROM izanami.pending_imports WHERE id=$$1 + |""".stripMargin, + List(id) + ){_ => Some(())}.map(_ => ()) + } + + def readImportStatus(id: UUID): Future[Option[ImportState]] = { + env.postgresql.queryOne( + s""" + |SELECT status, result FROM izanami.pending_imports WHERE id=$$1 + |""".stripMargin, + List(id) + ){r => { + val maybeStatus = r.optJsObject("result") + .flatMap(obj => obj.asOpt[ImportState](importResultReads)) + + maybeStatus.orElse(r.optString("status").filter(_ == "PENDING").map(_ => ImportPending(id))) + }} + } + + def markImportAsSucceded(id: UUID, importSuccess: ImportSuccess): Future[Unit] = { + env.postgresql.queryOne( + s""" + |UPDATE izanami.pending_imports SET status='FINISHED', result=$$2 WHERE id=$$1 + |""".stripMargin, + List(id, Json.toJson(importSuccess)(importSuccessWrites).vertxJsValue) + ){r => Some(())} + .map(_ => ()) + } + + def markImportAsFailed(id: UUID, importFailure: ImportFailure): Future[Unit] = { + env.postgresql.queryOne( + s""" + |UPDATE izanami.pending_imports SET status='FAILED', result=$$2 WHERE id=$$1 + |""".stripMargin, + List(id, Json.toJson(importFailure)(importFailureWrites).vertxJsValue) + ) { r => Some(()) } + .map(_ => ()) + } + + + def markImportAsStarted(): Future[Either[IzanamiError, UUID]] = { + env.postgresql.queryOne( + s"""INSERT INTO izanami.pending_imports DEFAULT VALUES RETURNING id""", + List() + ){r => r.optUUID("id")} + .map(_.toRight(InternalServerError())) + } + + def createTenant(tenantCreationRequest: TenantCreationRequest, user: String): Future[Either[IzanamiError, Tenant]] = { + val connectOptions = env.postgresql.connectOptions + val config = new HikariConfig() + config.setDriverClassName(classOf[org.postgresql.Driver].getName) + config.setJdbcUrl( + s"jdbc:postgresql://${connectOptions.getHost}:${connectOptions.getPort}/${connectOptions.getDatabase}" + ) + config.setUsername(connectOptions.getUser) + config.setPassword(connectOptions.getPassword) + config.setMaximumPoolSize(10) + val dataSource = new HikariDataSource(config) + val flyway = + Flyway.configure + .dataSource(dataSource) + .locations("filesystem:conf/sql/tenants", "filesystem:sql/tenants", "sql/tenants", "conf/sql/tenants") + .baselineOnMigrate(true) + .schemas(tenantCreationRequest.name) + .load() + flyway.migrate() + dataSource.close() + + env.postgresql.executeInTransaction(conn => { + env.postgresql + .queryOne( + s"insert into izanami.tenants (name, description) values ($$1, $$2) returning *", + List(tenantCreationRequest.name, tenantCreationRequest.description), + conn=Some(conn) + ) { row => row.optTenant() } + .map(maybeFeature => maybeFeature.toRight(InternalServerError())) + .recover { + case f: PgException if f.getSqlState == UNIQUE_VIOLATION => Left(TenantAlreadyExists(tenantCreationRequest.name)) + case _ => Left(InternalServerError()) + }.flatMap { + case Left(value) => Left(value).future + case Right(value) => env.postgresql.queryOne( + s""" + | INSERT INTO izanami.users_tenants_rights(username, tenant, level) VALUES ($$1, $$2, $$3) + | RETURNING username + |""".stripMargin, + List(user, value.name, RightLevels.Admin.toString.toUpperCase), + conn=Some(conn) + ){_ => Some(value)} + .map(maybeFeature => maybeFeature.toRight(InternalServerError())) + } + }) + + } + + def updateTenant(name: String, updateRequest: TenantCreationRequest): Future[Either[IzanamiError, Unit]] = { + env.postgresql.executeInTransaction(conn => { + env.postgresql.queryOne( + s""" + |UPDATE izanami.tenants SET description=$$1 WHERE name=$$2 RETURNING name + |""".stripMargin, + List(updateRequest.description, name), + conn=Some(conn) + ){r => r.optString("name")} + .map(o => o.toRight(TenantDoesNotExists(name)).map(_ => ())) + }) + } + + def readTenants(): Future[List[Tenant]] = { + env.postgresql.queryAll( + "SELECT name, description FROM izanami.tenants" + ) { row => row.optTenant() } + } + + def readTenantsFiltered(names: Set[String]): Future[List[Tenant]] = { + if(names.isEmpty) { + Future.successful(List()) + } else { + env.postgresql.queryAll( + s""" + |SELECT name, description + |FROM izanami.tenants + |WHERE name=ANY($$1)""".stripMargin, + List(names.toArray) + ) { row => row.optTenant() } + } + + } + + def readTenantByName(name: String): Future[Either[IzanamiError, Tenant]] = { + env.postgresql + .queryOne( + s"""SELECT t.name, t.description + |FROM izanami.tenants t + |WHERE t.name=$$1 + |""".stripMargin, + List(name) + ) { row => row.optTenant() } + .map { _.toRight(TenantDoesNotExists(name)) } + } + + def deleteTenant(name: String): Future[Either[IzanamiError, Unit]] = { + + env.postgresql.executeInTransaction(conn => { + env.postgresql + .queryOne( + s"""DELETE FROM izanami.tenants WHERE name=$$1 RETURNING name""".stripMargin, + List(name), + conn=Some(conn) + ) { r => r.optString("name") } + .map { + _.toRight(TenantDoesNotExists(name)) + }.flatMap { + case l@Left(value) => Left(value).future + case r@Right(deletedName) => env.postgresql.queryRaw( + s"""DROP SCHEMA "${deletedName}" CASCADE""", + conn=Some(conn) + ){_ => Some(())} + .map(_ => Right(())) + } + }) + + } +} + +object tenantImplicits { + implicit class TenantRow(val row: Row) extends AnyVal { + def optTenant(): Option[Tenant] = { + for ( + name <- row.optString("name"); + description <- row.optString("description") + ) yield Tenant(name = name, projects = List(), description = description) + } + } +} diff --git a/app/fr/maif/izanami/datastores/UsersDatastore.scala b/app/fr/maif/izanami/datastores/UsersDatastore.scala new file mode 100644 index 000000000..92ec71f53 --- /dev/null +++ b/app/fr/maif/izanami/datastores/UsersDatastore.scala @@ -0,0 +1,1401 @@ +package fr.maif.izanami.datastores + +import akka.actor.Cancellable +import fr.maif.izanami.datastores.userImplicits.{UserRow, dbUserTypeToUserType, rightRead} +import fr.maif.izanami.env.Env +import fr.maif.izanami.env.PostgresqlErrors.{RELATION_DOES_NOT_EXISTS, UNIQUE_VIOLATION} +import fr.maif.izanami.env.pgimplicits.EnhancedRow +import fr.maif.izanami.errors._ +import fr.maif.izanami.models.RightLevels.{RightLevel, superiorOrEqualLevels} +import fr.maif.izanami.models.Rights.TenantRightDiff +import fr.maif.izanami.models.User.{rightLevelReads, tenantRightReads} +import fr.maif.izanami.models._ +import fr.maif.izanami.utils.Datastore +import fr.maif.izanami.utils.syntax.implicits.BetterSyntax +import fr.maif.izanami.web.ImportController.{Fail, ImportConflictStrategy, MergeOverwrite, Skip} +import io.vertx.pgclient.PgException +import io.vertx.sqlclient.{Row, SqlConnection} +import play.api.libs.json.{JsError, JsSuccess, Reads} + +import scala.collection.mutable.ArrayBuffer +import scala.concurrent.Future +import scala.concurrent.duration.DurationInt + +class UsersDatastore(val env: Env) extends Datastore { + var sessionExpirationCancellation: Cancellable = Cancellable.alreadyCancelled + var invitationExpirationCancellation: Cancellable = Cancellable.alreadyCancelled + var passwordResetRequestCancellation: Cancellable = Cancellable.alreadyCancelled + + override def onStart(): Future[Unit] = { + sessionExpirationCancellation = env.actorSystem.scheduler.scheduleAtFixedRate(5.minutes, 5.minutes)(() => + deleteExpiredSessions(env.configuration.get[Int]("app.sessions.ttl")) + ) + invitationExpirationCancellation = env.actorSystem.scheduler.scheduleAtFixedRate(5.minutes, 5.minutes)(() => + deleteExpiredInvitations(env.configuration.get[Int]("app.invitations.ttl")) + ) + passwordResetRequestCancellation = env.actorSystem.scheduler.scheduleAtFixedRate(5.minutes, 5.minutes)(() => + deleteExpiredPasswordResetRequests(env.configuration.get[Int]("app.password-reset-requests.ttl")) + ) + Future.successful(()) + } + + override def onStop(): Future[Unit] = { + sessionExpirationCancellation.cancel() + invitationExpirationCancellation.cancel() + passwordResetRequestCancellation.cancel() + Future.successful(()) + } + + def createSession(username: String): Future[String] = { + env.postgresql + .queryOne(s"INSERT INTO izanami.sessions(username) VALUES ($$1) RETURNING id", List(username)) { row => + row.optUUID("id") + } + .map(maybeUUID => maybeUUID.getOrElse(throw new RuntimeException("Failed to create session")).toString) + } + + def deleteSession(sessionId: String): Future[Option[String]] = { + env.postgresql + .queryOne(s"DELETE FROM izanami.sessions WHERE id=$$1 RETURNING id", List(sessionId)) { row => + row.optUUID("id") + } + .map(maybeUUID => maybeUUID.map(_.toString)) + } + + def deleteExpiredSessions(sessiontTtlInSeconds: Integer): Future[Integer] = { + env.postgresql + .queryAll( + s"DELETE FROM izanami.sessions WHERE EXTRACT(EPOCH FROM (NOW() - creation)) > $$1 returning id", + List(sessiontTtlInSeconds) + ) { _ => + Some(()) + } + .map(_.size) + } + + def deleteExpiredInvitations(invitationsTtlInSeconds: Integer): Future[Integer] = { + env.postgresql + .queryAll( + s"DELETE FROM izanami.invitations WHERE EXTRACT(EPOCH FROM (NOW() - creation)) > $$1 returning id", + List(invitationsTtlInSeconds) + ) { _ => + Some(()) + } + .map(_.size) + } + + def deleteExpiredPasswordResetRequests(ttlInSeconds: Integer): Future[Integer] = { + env.postgresql + .queryAll( + s"DELETE FROM izanami.password_reset WHERE EXTRACT(EPOCH FROM (NOW() - creation)) > $$1 returning id", + List(ttlInSeconds) + ) { _ => + Some(()) + } + .map(_.size) + } + + def updateUserInformation( + name: String, + updateRequest: UserInformationUpdateRequest + ): Future[Either[IzanamiError, Unit]] = { + env.postgresql + .queryOne( + s"""UPDATE izanami.users SET username=$$1, email=$$2, default_tenant=$$4 WHERE username=$$3 RETURNING username""", + List(updateRequest.name, updateRequest.email, name, updateRequest.defaultTenant.orNull) + ) { _ => Some(()) } + .map(_.toRight(InternalServerError())) + .recover { + case f: PgException if f.getSqlState == UNIQUE_VIOLATION => + Left(UserAlreadyExist(updateRequest.name, updateRequest.email)) + case ex => + logger.error("Failed to user", ex) + Left(InternalServerError()) + } + } + + def updateLegacyUser( + name: String, + password: String + ): Future[Either[IzanamiError, Unit]] = { + env.postgresql + .queryOne( + s"""UPDATE izanami.users SET password=$$1, legacy=false WHERE username=$$2 RETURNING username""", + List(HashUtils.bcryptHash(password), name) + ) { _ => Some(()) } + .map(_.toRight(InvalidCredentials())) + } + + def updateUserPassword( + name: String, + password: String + ): Future[Either[IzanamiError, Unit]] = { + env.postgresql + .queryOne( + s"""UPDATE izanami.users SET password=$$1 WHERE username=$$2 RETURNING username""", + List(HashUtils.bcryptHash(password), name) + ) { _ => Some(()) } + .map(_.toRight(InvalidCredentials())) + } + + def deleteRightsForProject(username: String, tenant: String, project: String): Future[Unit] = { + env.postgresql + .queryOne( + s""" + |DELETE FROM users_projects_rights WHERE username=$$1 + |""".stripMargin, + List(username), + schemas = Set(tenant) + ) { _ => Some(()) } + .map(_ => ()) + } + + def deleteRightsForTenant( + name: String, + tenant: String, + loggedInUser: UserWithRights + ): Future[Either[IzanamiError, Unit]] = { + val authorized = loggedInUser.hasAdminRightForTenant(tenant) + + if (!authorized) { + Left(NotEnoughRights()).future + } else { + env.postgresql.executeInTransaction( + conn => + { + env.postgresql + .queryOne( + s""" + |DELETE FROM izanami.users_tenants_rights WHERE username=$$1 AND tenant=$$2 + |""".stripMargin, + List(name, tenant), + conn = Some(conn) + ) { _ => Some(()) } + .flatMap(_ => { + env.postgresql.queryOne( + s""" + |DELETE FROM users_projects_rights WHERE username=$$1 + |""".stripMargin, + List(name), + conn = Some(conn) + ) { r => Some(()) } + }) + .flatMap(_ => { + env.postgresql.queryOne( + s""" + |DELETE FROM users_keys_rights WHERE username=$$1 + |""".stripMargin, + List(name), + conn = Some(conn) + ) { r => Some(()) } + }) + }.map(_ => Right(())), + schemas = Set(tenant) + ) + } + } + + def updateUserRightsForProject( + username: String, + tenant: String, + project: String, + right: RightLevel + ): Future[Unit] = { + env.postgresql + .queryOne( + s""" + |INSERT INTO users_projects_rights (username, project, level) VALUES ($$1, $$2, $$3) + |ON CONFLICT(username, project) DO UPDATE + |SET username=EXCLUDED.username, project=EXCLUDED.project, level=EXCLUDED.level + |RETURNING 1 + |""".stripMargin, + List(username, project, right.toString.toUpperCase), + schemas = Set(tenant) + ) { _ => Some(()) } + .map(_ => ()) + } + + def updateUserRightsForTenant( + name: String, + tenant: String, + diff: TenantRightDiff + ): Future[Unit] = { + env.postgresql + .executeInTransaction( + conn => { + val tenantQuery = diff.removedTenantRight + .map(r => { + env.postgresql.queryOne( + s""" + |DELETE FROM izanami.users_tenants_rights + |WHERE username=$$1 + |AND tenant=$$2 + |RETURNING username + |""".stripMargin, + List(name, tenant), + conn = Some(conn) + ) { _ => Some(()) } + }) + .toSeq + Future + .sequence( + tenantQuery.concat( + Seq( + env.postgresql.queryOne( + s""" + |DELETE FROM users_projects_rights + |WHERE username=$$1 + |AND project=ANY($$2) + |RETURNING username + |""".stripMargin, + List(name, diff.removedProjectRights.map(_.name).toArray), + conn = Some(conn) + ) { _ => Some(()) }, + env.postgresql.queryOne( + s""" + |DELETE FROM users_keys_rights + |WHERE username=$$1 + |AND apikey=ANY($$2) + |RETURNING username + |""".stripMargin, + List(name, diff.removedKeyRights.map(_.name).toArray), + conn = Some(conn) + ) { _ => Some(()) } + ) + ) + ) + .flatMap(_ => { + Future.sequence( + ( + diff.addedTenantRight + .map(r => { + env.postgresql.queryOne( + s""" + |INSERT INTO izanami.users_tenants_rights(username, tenant, level) + |VALUES($$1, $$2, $$3) + |RETURNING username + |""".stripMargin, + List( + name, + tenant, + r.level.toString.toUpperCase + ), + conn = Some(conn) + ) { _ => Some(()) } + }) + ) + .toSeq + .concat( + Seq( + env.postgresql.queryOne( + s""" + |INSERT INTO users_projects_rights(username, project, level) + |VALUES($$1, unnest($$2::TEXT[]), unnest($$3::izanami.right_level[])) + |RETURNING username + |""".stripMargin, + List( + name, + diff.addedProjectRights.map { flatten => flatten.name }.toArray, + diff.addedProjectRights.map { flatten => flatten.level.toString.toUpperCase }.toArray + ), + conn = Some(conn) + ) { _ => Some(()) }, + env.postgresql.queryOne( + s""" + |INSERT INTO users_keys_rights(username,apikey, level) + |VALUES($$1, unnest($$2::TEXT[]), unnest($$3::izanami.right_level[])) + |RETURNING username + |""".stripMargin, + List( + name, + diff.addedKeyRights.map(flatten => flatten.name).toArray, + diff.addedKeyRights.map(flatten => flatten.level.toString.toUpperCase).toArray + ), + conn = Some(conn) + ) { _ => Some(()) } + ) + ) + ) + }) + }, + schemas = Set(tenant) + ) + .map(_ => ()) + } + + def updateUserRights( + name: String, + updateRequest: UserRightsUpdateRequest + ): Future[Either[IzanamiError, Unit]] = { + findUserWithCompleteRights(name) + .flatMap { + case Some(UserWithRights(_, _, _, _, _, rights, _, _)) => { + val diff = Rights.compare(base = rights, modified = updateRequest.rights) + // TODO externalize this + env.postgresql.executeInTransaction(conn => { + updateRequest.admin + .map(admin => + env.postgresql + .queryOne( + s"""UPDATE izanami.users SET admin=$$1 WHERE username=$$2 RETURNING username""", + List(java.lang.Boolean.valueOf(admin), name), + conn = Some(conn) + ) { _ => Some(()) } + ) + .getOrElse(Future(Some(()))) + .map(_.toRight(InternalServerError())) + .flatMap { + case Left(value) => Left(value).future + case Right(_) => { + env.postgresql + .queryOne( + s""" + |DELETE FROM izanami.users_tenants_rights + |WHERE username=$$1 + |AND tenant=ANY($$2) + |RETURNING username + |""".stripMargin, + List(name, diff.removedTenantRights.map(_.name).toArray), + conn = Some(conn) + ) { _ => Some(()) } + .flatMap(_ => { + diff.removedProjectRights.foldLeft(Future.successful(())) { case (f, (tenant, rights)) => + f.flatMap(_ => env.postgresql.updateSearchPath(tenant, conn)) + .flatMap(_ => + env.postgresql + .queryOne( + s""" + |DELETE FROM users_projects_rights + |WHERE username=$$1 + |AND project=ANY($$2) + |RETURNING username + |""".stripMargin, + List(name, rights.map(_.name).toArray), + conn = Some(conn) + ) { _ => Some(()) } + .map(_ => ()) + ) + } + }) + .flatMap(_ => { + diff.removedKeyRights.foldLeft(Future.successful(())) { case (f, (tenant, rights)) => + f.flatMap(_ => env.postgresql.updateSearchPath(tenant, conn)) + .flatMap(_ => + env.postgresql + .queryOne( + s""" + |DELETE FROM users_keys_rights + |WHERE username=$$1 + |AND apikey=ANY($$2) + |RETURNING username + |""".stripMargin, + List(name, rights.map(_.name).toArray), + conn = Some(conn) + ) { _ => Some(()) } + .map(_ => ()) + ) + } + }) + .flatMap(_ => { + env.postgresql + .queryOne( + s""" + |INSERT INTO izanami.users_tenants_rights(username, tenant, level) + |VALUES($$1, unnest($$2::TEXT[]), unnest($$3::izanami.right_level[])) + |RETURNING username + |""".stripMargin, + List( + name, + diff.addedTenantRights.map(_.name).toArray, + diff.addedTenantRights.map(_.level.toString.toUpperCase).toArray + ), + conn = Some(conn) + ) { _ => Some(()) } + .flatMap(_ => { + diff.addedProjectRights.foldLeft(Future.successful(())) { case (f, (tenantName, rights)) => + f.flatMap(_ => env.postgresql.updateSearchPath(tenantName, conn)) + .flatMap(_ => + env.postgresql + .queryOne( + s""" + |INSERT INTO users_projects_rights(username, project, level) + |VALUES($$1, unnest($$2::TEXT[]), unnest($$3::izanami.right_level[])) + |RETURNING username + |""".stripMargin, + List( + name, + rights.map(_.name).toArray, + rights.map(_.level.toString.toUpperCase).toArray + ), + conn = Some(conn) + ) { _ => Some(()) } + .map(_ => ()) + ) + } + }) + .flatMap(_ => { + diff.addedKeyRights.foldLeft(Future.successful(())) { case (f, (tenantName, rights)) => + f.flatMap(_ => env.postgresql.updateSearchPath(tenantName, conn)) + .flatMap(_ => + env.postgresql + .queryOne( + s""" + |INSERT INTO users_keys_rights(username,apikey, level) + |VALUES($$1, unnest($$2::TEXT[]), unnest($$3::izanami.right_level[])) + |RETURNING username + |""".stripMargin, + List( + name, + rights.map(_.name).toArray, + rights.map(_.level.toString.toUpperCase).toArray + ), + conn = Some(conn) + ) { _ => Some(()) } + .map(_ => ()) + ) + } + }) + }) + .map(_ => Right(())) + } + } + }) + } + case None => Left(UserNotFound(name)).future + } + } + + def createUserWithConn( + users: Seq[UserWithRights], + conn: SqlConnection, + importConflictStrategy: ImportConflictStrategy = Fail + ): Future[Either[IzanamiError, Unit]] = { + if(users.isEmpty) { + Future.successful(Right(())) + } else { + val eventualErrorOrUnit: Future[Either[InternalServerError, Unit]] = env.postgresql + .queryRaw( + s"""insert into izanami.users (username, password, admin, email, user_type, legacy) + |values (unnest($$1::TEXT[]), unnest($$2::TEXT[]), unnest($$3::BOOLEAN[]), unnest($$4::TEXT[]), unnest($$5::izanami.user_type[]), unnest($$6::BOOLEAN[])) ${ + importConflictStrategy match { + case Fail => "" + case MergeOverwrite => s" ON CONFLICT(username) DO UPDATE SET admin=COALESCE(users.admin, excluded.admin)" + case Skip => " ON CONFLICT(username) DO NOTHING" + } + } returning *""".stripMargin, + List( + users.map(_.username).toArray, + users.map(user => Option(user.password).map(pwd => HashUtils.bcryptHash(pwd)).orNull).toArray, + users.map(user => java.lang.Boolean.valueOf(user.admin)).toArray, + users.map(user => Option(user.email).orNull).toArray, + users.map(user => user.userType.toString).toArray, + users.map(user => java.lang.Boolean.valueOf(user.legacy)).toArray + ), + conn = Some(conn) + ) { _ => Some(()) } + .flatMap(_ => { + val base = users.flatMap(u => u.rights.tenants.map { case (tenant, right) => (tenant, u.username, right) }) + base + .filter { case (_, _, r) => r.projects.nonEmpty } + .flatMap { + case (tenant, username, tenantRight) => { + tenantRight.projects.map { case (project, right) => + (tenant, username, project, right.level) + } + } + }.groupBy(_._1).view.mapValues(seq => seq.map { case (_, username, project, level) => (username, project, level) }) + .foldLeft(Future.successful(())) { case (future, (tenant, values)) => { + future.flatMap(_ => createProjectRights(tenant, values, conn, importConflictStrategy)) + } + }.flatMap(_ => { + base + .filter { case (_, _, r) => r.keys.nonEmpty } + .flatMap { + case (tenant, username, tenantRight) => { + tenantRight.keys.map { case (key, right) => + (tenant, username, key, right.level) + } + } + }.groupBy(_._1).view.mapValues(seq => seq.map { case (_, username, key, level) => (username, key, level) }) + .foldLeft(Future.successful(())) { case (future, (tenant, values)) => { + future.flatMap(_ => createKeyRights(tenant, values, conn, importConflictStrategy)) + } + } + }).flatMap(_ => { + val (usernames, tenants, levels) = users + .flatMap(u => { + u.rights.tenants.map { case (tenant, TenantRight(level, _, _)) => (u.username, tenant, level) } + }) + .toArray + .unzip3 + + env.postgresql.queryRaw( + s""" + |INSERT INTO izanami.users_tenants_rights (username, tenant, level) + |VALUES (unnest($$1::TEXT[]), unnest($$2::TEXT[]), unnest($$3::izanami.right_level[])) + |${ + importConflictStrategy match { + case Fail => "" + case MergeOverwrite => + """ + | ON CONFLICT(username, tenant) DO UPDATE SET level = CASE + | WHEN users_keys_rights.level = 'READ' THEN excluded.level + | WHEN (users_keys_rights.level = 'WRITE' AND excluded.level = 'ADMIN') THEN 'ADMIN' + | WHEN users_keys_rights.level = 'ADMIN' THEN 'ADMIN' + | ELSE users_keys_rights.level + | END + |""".stripMargin + case Skip => " ON CONFLICT(username, tenant) DO NOTHING " + } + } + |returning username + |""".stripMargin, + List(usernames, tenants, levels.map(l => l.toString.toUpperCase())), + conn = Some(conn) + ) { _ => Some(()) } + }) + }) + .map(_ => Right(())) + .recover { + case _ => Left(InternalServerError()) + } + eventualErrorOrUnit + } + } + + + def createUser(user: UserWithRights): Future[Either[IzanamiError, Unit]] = { + env.postgresql.executeInTransaction(conn => { + createUserWithConn(Seq(user), conn) + }) + } + + def deleteUser(username: String): Future[Unit] = { + env.postgresql + .queryOne( + s""" + |DELETE FROM izanami.users + |WHERE username=$$1 + |""".stripMargin, + List(username) + ) { row => + { + Some(()) + } + } + .map(o => o.getOrElse(())) + } + + def hasRightForKey( + session: String, + tenant: String, + key: String, + level: RightLevel + ): Future[Either[IzanamiError, Option[String]]] = { + env.postgresql + .queryOne( + s""" + |SELECT u.username + |FROM izanami.sessions s + |LEFT JOIN izanami.users u ON u.username=s.username + |LEFT JOIN izanami.users_tenants_rights utr ON u.username = utr.username AND utr.tenant=$$2 + |LEFT JOIN users_keys_rights ukr ON u.username = ukr.username AND ukr.apikey=$$3 + |WHERE s.id=$$1 + |AND ( + | u.admin=true + | OR utr.level='ADMIN' + | OR ukr.level=ANY($$4) + |) + |""".stripMargin, + List(session, tenant, key, superiorOrEqualLevels(level).map(l => l.toString.toUpperCase).toArray), + schemas = Set(tenant) + ) { r => r.optString("username") } + .map(Right(_)) + .recover { + case f: PgException if f.getSqlState == RELATION_DOES_NOT_EXISTS => Left(TenantDoesNotExists(tenant)) + case _ => Left(InternalServerError()) + } + } + + // TODO merge with hasRightFor ? + def hasRightForProject( + session: String, + tenant: String, + project: String, + level: RightLevel + ): Future[Either[IzanamiError, Boolean]] = { + env.postgresql + .queryOne( + s""" + |SELECT u.username + |FROM izanami.sessions s + |LEFT JOIN izanami.users u ON u.username=s.username + |LEFT JOIN izanami.users_tenants_rights utr ON u.username = utr.username AND utr.tenant=$$2 + |LEFT JOIN users_projects_rights upr ON u.username = upr.username AND upr.project=$$3 + |WHERE s.id=$$1 + |AND ( + | u.admin=true + | OR utr.level='ADMIN' + | OR upr.level=ANY($$4) + |) + |""".stripMargin, + List(session, tenant, project, superiorOrEqualLevels(level).map(l => l.toString.toUpperCase).toArray), + schemas = Set(tenant) + ) { _ => Some(true) } + .map(maybeBoolean => maybeBoolean.getOrElse(false)) + .map(Right(_)) + .recover { + case f: PgException if f.getSqlState == RELATION_DOES_NOT_EXISTS => Left(TenantDoesNotExists(tenant)) + case _ => Left(InternalServerError()) + } + } + + // TODO merge with hasRight + def hasRightForTenant(session: String, tenant: String, level: RightLevel): Future[Option[String]] = { + env.postgresql + .queryOne( + s""" + |SELECT u.username + |FROM izanami.sessions s + |LEFT JOIN izanami.users u ON u.username = s.username + |LEFT JOIN izanami.users_tenants_rights utr ON u.username = utr.username AND utr.tenant=$$2 + |WHERE s.id=$$1 + |AND ( + | u.admin=true + | OR utr.level=ANY($$3) + |) + |""".stripMargin, + List(session, tenant, superiorOrEqualLevels(level).map(l => l.toString.toUpperCase).toArray) + ) { r => r.optString("username") } + } + + def hasRightFor( + tenant: String, + username: String, + rights: Set[RightUnit], + tenantLevel: Option[RightLevel] = Option.empty + ) = { + val (keys, projects): (Set[String], Set[String]) = rights.partitionMap(r => { + r.rightType match { + case RightTypes.Key => Left(r.name) + case RightTypes.Project => Right(r.name) + } + }) + + var index = 2 + val subQueries = ArrayBuffer[String]() + val params = ArrayBuffer[Object](username) + + if (projects.nonEmpty) { + subQueries.addOne(s""" + |'projects', + |array( + | select json_build_object('name', p.project, 'level', p.level) + | from users_projects_rights p + | where p.username=$$1 + | and p.project=ANY($$${index}) + |) + |""".stripMargin) + index = index + 1 + params.addOne(projects.toArray) + } + if (keys.nonEmpty) { + subQueries.addOne( + s""" + |'keys', + |array( + | select json_build_object('name', k.apikey, 'level', k.level) + | from users_keys_rights k + | where k.username=$$1 + | and k.apikey=ANY($$${index}) + |) + |""".stripMargin + ) + index = index + 1 + params.addOne(keys.toArray) + } + + params.addOne(tenant) + + env.postgresql + .queryOne( + s""" + |SELECT utr.level, u.admin, json_build_object( + |${subQueries.mkString(",")} + |)::jsonb as rights + |FROM izanami.users u + |LEFT JOIN izanami.users_tenants_rights utr ON u.username = utr.username AND utr.tenant=$$${index} + |WHERE u.username=$$1 + |""".stripMargin, + params.toList, + schemas = Set(tenant) + ) { r => + { + val admin = r.getBoolean("admin") + val tenantRightLevel = r.optRightLevel("level") + val extractedRights = r + .optJsObject("rights") + .map(obj => { + (obj \ "projects") + .asOpt[Set[RightValue]] + .getOrElse(Set()) + .map(r => (RightTypes.Project, r.level, r.name)) + .concat( + (obj \ "keys").asOpt[Set[RightValue]].getOrElse(Set()).map(r => (RightTypes.Key, r.level, r.name)) + ) + }) + .getOrElse(Set()) + .groupBy(t => (t._1, t._3)) + .view + .mapValues(s => s.map(t => t._2)) + + val projectKeyRightMatches = rights + .map(r => (r.rightType, r.rightLevel, r.name)) + .forall(t => { + val maybeExtractedLevels = extractedRights.get((t._1, t._3)) + val acceptableLevels = RightLevels.superiorOrEqualLevels(t._2) + maybeExtractedLevels + .exists(levels => levels.intersect(acceptableLevels).nonEmpty) + }) + Some( + admin || + tenantRightLevel.contains(RightLevels.Admin) || + tenantLevel + .map(tLevel => { + tenantRightLevel.exists(extractedLevel => + superiorOrEqualLevels(tLevel).contains(extractedLevel) + ) && projectKeyRightMatches + }) + .getOrElse(projectKeyRightMatches) + ) + } + } + .map(o => o.getOrElse(false)) + } + + def findAdminSession(session: String): Future[Option[String]] = { + env.postgresql + .queryOne( + s""" + |SELECT u.username + |FROM izanami.users u, izanami.sessions s + |WHERE u.username = s.username + |AND s.id = $$1 + |AND u.admin = true + |""".stripMargin, + List(session) + ) { row => row.optString("username") } + } + + def findSession(session: String): Future[Option[String]] = { + env.postgresql + .queryOne( + s""" + |SELECT u.username + |FROM izanami.users u, izanami.sessions s + |WHERE u.username = s.username + |AND s.id = $$1 + |""".stripMargin, + List(session) + ) { row => row.optString("username") } + } + + def isAdmin(username: String): Future[Boolean] = { + env.postgresql + .queryOne( + s""" + |SELECT u.username + |FROM izanami.users u + |WHERE u.username = $$1 + |AND u.admin = true + |""".stripMargin, + List(username) + ) { _ => Some(true) } + .map(maybeBoolean => maybeBoolean.getOrElse(false)) + } + + def createInvitation( + email: String, + admin: Boolean, + rights: Rights, + inviter: String + ): Future[Either[IzanamiError, String]] = { + env.postgresql + .queryOne( + s""" + |INSERT INTO izanami.invitations(email, admin, rights, inviter) values ($$1, $$2, $$3::jsonb, $$4) + |ON CONFLICT(email) + |DO UPDATE + |SET admin=EXCLUDED.admin, rights=EXCLUDED.rights, creation=EXCLUDED.creation, id=EXCLUDED.id, inviter=EXCLUDED.inviter + |returning id + |""".stripMargin, + List(email, java.lang.Boolean.valueOf(admin), User.rightWrite.writes(rights).toString(), inviter) + ) { row => + Some(row.getUUID("id").toString) + } + .map(o => o.toRight(InternalServerError())) + } + + def deleteInvitation(id: String): Future[Option[Unit]] = { + env.postgresql.queryOne( + s""" + |DELETE FROM izanami.invitations WHERE id=$$1::UUID RETURNING id + |""".stripMargin, + List(id) + ) { _ => + Some(()) + } + } + + def readInvitation(id: String): Future[Option[UserInvitation]] = { + env.postgresql.queryOne( + s""" + |SELECT id, email, admin, rights from izanami.invitations where id=$$1 + |""".stripMargin, + List(id) + ) { row => + { + for ( + id <- row.optUUID("id"); + admin <- row.optBoolean("admin"); + jsonRights <- row.optJsObject("rights"); + rights <- User.rightsReads.reads(jsonRights).asOpt + ) yield UserInvitation(email = row.optString("email").orNull, admin = admin, rights = rights, id = id.toString) + } + } + } + + def createProjectRights( + tenant: String, + rights: Seq[(String, String, RightLevel)], + conn: SqlConnection, + conflictStrategy: ImportConflictStrategy = Fail + ): Future[Unit] = { + val (usernames, projects, levels) = rights + .toArray + .map{case (username, project, right) => (username, project, right.toString.toUpperCase)} + .unzip3 + + env.postgresql.queryRaw( + s""" + |INSERT INTO users_projects_rights(username, project, level) + |VALUES (unnest($$1::TEXT[]), unnest($$2::TEXT[]), unnest($$3::izanami.right_level[])) + |${conflictStrategy match { + case Fail => "" + case MergeOverwrite => """ + | ON CONFLICT(username, project) DO UPDATE SET level=CASE + | WHEN users_keys_rights.level='READ' THEN excluded.level + | WHEN (users_keys_rights.level='WRITE' AND excluded.level = 'ADMIN') THEN 'ADMIN' + | WHEN users_keys_rights.level='ADMIN' THEN 'ADMIN' + | ELSE users_keys_rights.level + |END + |""".stripMargin + case Skip => " ON CONFLICT(username, project) DO NOTHING " + }} + |RETURNING username + |""".stripMargin, + List(usernames, projects, levels), + schemas=Set(tenant), + conn=Some(conn) + ){_ => Some(())} + } + + def createKeyRights( + tenant: String, + rights: Seq[(String, String, RightLevel)], + conn: SqlConnection, + conflictStrategy: ImportConflictStrategy = Fail + ): Future[Unit] = { + val (usernames, projects, levels) = rights + .toArray + .map { case (username, project, right) => (username, project, right.toString.toUpperCase) } + .unzip3 + + env.postgresql.queryRaw( + s""" + |INSERT INTO users_keys_rights(username, apikey, level) + |VALUES (unnest($$1::TEXT[]), unnest($$2::TEXT[]), unnest($$3::izanami.right_level[])) + |${conflictStrategy match { + case Fail => "" + case MergeOverwrite => + """ + | ON CONFLICT(username, project) DO UPDATE SET level = CASE + | WHEN users_keys_rights.level = 'READ' THEN excluded.level + | WHEN (users_keys_rights.level = 'WRITE' AND excluded.level = 'ADMIN') THEN 'ADMIN' + | WHEN users_keys_rights.level = 'ADMIN' THEN 'ADMIN' + | ELSE users_keys_rights.level + | END + |""".stripMargin + case Skip => " ON CONFLICT(username, project) DO NOTHING " + }} + |RETURNING username + |""".stripMargin, + List(usernames, projects, levels), + schemas = Set(tenant), + conn = Some(conn) + ) { _ => () } + } + + def isUserValid(username: String, password: String): Future[Option[User]] = { + // TODO handle lecgacy users & test it ! + env.postgresql + .queryOne( + s"""SELECT username, password, admin, email, user_type, legacy FROM izanami.users WHERE username=$$1""", + List(username) + ) { row => + row + .optString("password") + .filter(hashed => { + row.optBoolean("legacy").exists { + case true => HashUtils.bcryptCheck(HashUtils.hexSha512(password), hashed) + case false => HashUtils.bcryptCheck(password, hashed) + } + }) + .flatMap(_ => row.optUser().map(u => u.copy(legacy = row.optBoolean("legacy").getOrElse(false)))) + } + } + + def findSessionWithTenantRights(session: String): Future[Option[UserWithTenantRights]] = { + env.postgresql.queryOne( + s""" + |SELECT u.username, u.admin, u.email, u.default_tenant, u.user_type, + | coalesce(( + | select json_object_agg(utr.tenant, utr.level) + | from izanami.users_tenants_rights utr + | where utr.username=u.username + | ), '{}'::json) as tenants + |from izanami.users u, izanami.sessions s + |WHERE u.username=s.username + |AND s.id=$$1""".stripMargin, + List(session) + ) { row => + { + for ( + username <- row.optString("username"); + admin <- row.optBoolean("admin"); + rights <- row.optJsObject("tenants"); + userType <- row.optString("user_type").map(dbUserTypeToUserType) + ) yield { + val tenantRights = rights.asOpt[Map[String, RightLevel]].getOrElse(Map()) + UserWithTenantRights( + username = username, + email = row.optString("email").orNull, + password = null, + admin = admin, + tenantRights = tenantRights, + userType = userType + ) + } + } + } + } + + def savePasswordResetRequest(username: String): Future[String] = { + env.postgresql + .queryOne( + s""" + |INSERT into izanami.password_reset(username) + |VALUES($$1) + |ON CONFLICT(username) + |DO UPDATE + |SET creation=EXCLUDED.creation, id=EXCLUDED.id + |RETURNING id + |""".stripMargin, + List(username) + ) { row => + row.optUUID("id").map(_.toString) + } + .map(_.getOrElse(throw new RuntimeException("Failed to create password request"))) + } + + def findPasswordResetRequest(id: String): Future[Option[String]] = { + env.postgresql.queryOne( + s""" + |SELECT username FROM izanami.password_reset WHERE id=$$1 + |""".stripMargin, + List(id) + ) { row => row.optString("username") } + } + + def deletePasswordResetRequest(id: String): Future[Unit] = { + env.postgresql + .queryOne( + s""" + |DELETE FROM izanami.password_reset WHERE id=$$1 + |""".stripMargin, + List(id) + ) { _ => Some(()) } + .map(_ => ()) + } + + def findUserByMail(email: String): Future[Option[User]] = { + env.postgresql.queryOne( + s""" + |SELECT username, email, user_type, admin, default_tenant FROM izanami.users WHERE email=$$1 + |""".stripMargin, + List(email) + ) { r => r.optUser() } + } + + def findUser(username: String): Future[Option[UserWithTenantRights]] = { + env.postgresql.queryOne( + s""" + |SELECT username, admin, email, user_type, default_tenant, + | coalesce(( + | select json_object_agg(utr.tenant, utr.level) + | from izanami.users_tenants_rights utr + | where utr.username=$$1 + | ), '{}'::json) as tenants + |from izanami.users + |WHERE username=$$1""".stripMargin, + List(username) + ) { row => + { + for ( + username <- row.optString("username"); + admin <- row.optBoolean("admin"); + rights <- row.optJsObject("tenants"); + userType <- row.optString("user_type").map(dbUserTypeToUserType) + ) yield { + val tenantRights = rights.asOpt[Map[String, RightLevel]].getOrElse(Map()) + UserWithTenantRights( + username = username, + email = row.optString("email").orNull, + password = null, + admin = admin, + tenantRights = tenantRights, + defaultTenant = row.optString("default_tenant"), + userType = userType + ) + } + } + } + } + + def searchUsers(search: String, count: Integer): Future[Seq[String]] = { + // TODO better matching algorithm + env.postgresql.queryAll( + s""" + |SELECT username + |FROM izanami.users + |WHERE username ILIKE $$1::TEXT + |ORDER BY LENGTH(username) + |LIMIT $$2 + |""".stripMargin, + List(s"%${search}%", count) + ) { r => r.optString("username") } + } + + def findUsers(username: String): Future[Set[UserWithTenantRights]] = { + env.postgresql + .queryAll( + s""" + |WITH rights AS ( + | SELECT utr.tenant, u.admin + | FROM izanami.users u + | LEFT JOIN izanami.users_tenants_rights utr ON utr.username=u.username + | WHERE u.username=$$1 + |) + |SELECT u.username, u.admin, u.email, u.user_type, u.default_tenant, + | CASE WHEN (SELECT admin FROM rights LIMIT 1) THEN ( + | SELECT coalesce(( + | select json_object_agg(utr2.tenant, utr2.level) + | from izanami.users_tenants_rights utr2 + | where utr2.username = u.username + | ), '{}'::json)) + | ELSE ( + | SELECT coalesce(( + | select json_object_agg(utr2.tenant, utr2.level) + | from izanami.users_tenants_rights utr2 + | where utr2.tenant=ANY(SELECT tenant FROM rights) + | and utr2.username=u.username + | ), '{}'::json) + | ) + | END AS tenants + | FROM izanami.users u + | GROUP BY (u.username, u.admin) + """.stripMargin, + List(username) + ) { row => + { + row.optUserWithTenantRights() + } + } + .map(users => users.toSet) + } + + def findUsersForTenant(tenant: String): Future[List[UserWithSingleLevelRight]] = { + env.postgresql.queryAll( + s""" + |SELECT u.username, u.email, u.admin, u.user_type, u.default_tenant, r.level + |FROM izanami.users u + |LEFT JOIN izanami.users_tenants_rights r ON r.username = u.username AND r.tenant=$$1 + |WHERE r.level IS NOT NULL + |OR u.admin=true + |""".stripMargin, + List(tenant) + ) { r => + r.optUser().map(u => u.withSingleLevelRight(r.optRightLevel("level").orNull)) + } + } + + def findUsersForProject(tenant: String, project: String): Future[List[UserWithSingleProjectRight]] = { + env.postgresql.queryAll( + s""" + |SELECT u.username, u.email, u.admin, u.user_type, u.default_tenant, r.level, tr.level as tenant_right + |FROM izanami.users u + |LEFT JOIN users_projects_rights r ON r.username = u.username AND r.project=$$1 + |LEFT JOIN izanami.users_tenants_rights tr ON tr.username = u.username AND tr.tenant=$$2 + |WHERE r.level IS NOT NULL + |OR tr.level='ADMIN' + |OR u.admin=true + |""".stripMargin, + List(project, tenant), + schemas = Set(tenant) + ) { r => + r.optUser() + .map(u => { + val maybeTenantRight = r.optRightLevel("tenant_right") + u.withSingleProjectRightLevel(r.optRightLevel("level").orNull, maybeTenantRight.contains(RightLevels.Admin)) + }) + } + } + + def findSessionWithCompleteRights(session: String): Future[Option[UserWithRights]] = { + findSessionWithTenantRights(session).flatMap { + case Some(user) if user.tenantRights.nonEmpty => { + val tenants = user.tenantRights.keys.toSet + findCompleteRightsFromTenant(user.username, tenants) + } + case Some(user) => Some(user.withRights(Rights.EMPTY)).future + case _ => Future.successful(None) + } + } + + def findCompleteRightsFromTenant(username: String, tenants: Set[String]): Future[Option[UserWithRights]] = { + Future + .sequence( + tenants.map(tenant => { + env.postgresql + .queryOne( + s""" + |SELECT u.username, u.admin, u.email, u.user_type, u.default_tenant, json_build_object( + | 'level', utr.level, + | 'projects', COALESCE((select json_object_agg(p.project, json_build_object('level', p.level)) from users_projects_rights p where p.username=$$1), '{}'), + | 'keys', COALESCE((select json_object_agg(k.apikey, json_build_object('level', k.level)) from users_keys_rights k where k.username=$$1), '{}') + |)::jsonb as rights + |from izanami.users u, izanami.users_tenants_rights utr + |WHERE u.username=$$1 AND utr.username=$$1 AND utr.tenant=$$2; + |""".stripMargin, + List(username, tenant), + schemas = Set(tenant) + ) { row => row.optUserWithRights() } + .map(fuser => fuser.map(u => (tenant, u))) + }) + ) + .map(users => { + val userParts = users.flatMap(o => o.toSeq) + val rightMap = userParts + .map { case (t, u) => (t, u.tenantRight) } + .filter { case (_, maybeRight) => maybeRight.isDefined } + .map { case (t, o) => (t, o.get) } + .toMap + + userParts.headOption + .map { case (_, u) => u } + .map(u => + UserWithRights( + username = u.username, + email = u.email, + admin = u.admin, + userType = u.userType, + rights = Rights(rightMap), + defaultTenant = u.defaultTenant + ) + ) + }) + } + + def findUserWithCompleteRights(username: String): Future[Option[UserWithRights]] = { + findUser(username).flatMap { + case Some(user) if user.tenantRights.nonEmpty => { + val tenants = user.tenantRights.keys.toSet + findCompleteRightsFromTenant(username, tenants) + } + case Some(user) => Some(user.withRights(Rights.EMPTY)).future + case _ => Future.successful(None) + } + } + + def addUserRightsToTenant(tenant: String, users: Seq[(String, RightLevel)]): Future[Unit] = { + env.postgresql + .queryOne( + s""" + |INSERT INTO izanami.users_tenants_rights (tenant, username, level) + |VALUES($$1, unnest($$2::TEXT[]), unnest($$3::izanami.right_level[])) + |ON CONFLICT (username, tenant) DO NOTHING + |""".stripMargin, + List(tenant, users.map(_._1).toArray, users.map(_._2.toString.toUpperCase).toArray) + ) { r => Some(()) } + .map(_ => ()) + } + + def addUserRightsToProject(tenant: String, project: String, users: Seq[(String, RightLevel)]): Future[Unit] = { + env.postgresql + .queryOne( + s""" + |INSERT INTO users_projects_rights (project, username, level) + |VALUES($$1, unnest($$2::TEXT[]), unnest($$3::izanami.right_level[])) + |ON CONFLICT (username, project) DO NOTHING + |""".stripMargin, + List(project, users.map(_._1).toArray, users.map(_._2.toString.toUpperCase).toArray), + schemas = Set(tenant) + ) { r => Some(()) } + .map(_ => ()) + } + + def findSessionWithRightForTenant( + session: String, + tenant: String + ): Future[Either[IzanamiError, UserWithCompleteRightForOneTenant]] = { + env.postgresql + .queryOne( + s""" + |SELECT u.username, u.admin, u.email, u.user_type, u.default_tenant, + | COALESCE(( + | select (json_build_object('level', utr.level, 'projects', ( + | select json_object_agg(p.project, json_build_object('level', p.level)) + | from users_projects_rights p + | where p.username=u.username + | ))) + | from izanami.users_tenants_rights utr + | where utr.username=u.username + | and utr.tenant=$$2 + | ), '{}'::json) as rights + |from izanami.users u, izanami.sessions s + |WHERE u.username=s.username + |and s.id=$$1""".stripMargin, + List(session, tenant), + schemas = Set(tenant) + ) { row => + { + for ( + username <- row.optString("username"); + userType <- row.optString("user_type").map(dbUserTypeToUserType); + admin <- row.optBoolean("admin"); + right <- row.optJsObject("rights") + ) yield { + val parsedRights = right.asOpt[TenantRight] + UserWithCompleteRightForOneTenant( + username = username, + email = row.optString("email").orNull, + password = null, + admin = admin, + tenantRight = parsedRights, + userType = userType + ) + } + } + } + .map(o => o.toRight(SessionNotFound(session))) + .recover { + case f: PgException if f.getSqlState == RELATION_DOES_NOT_EXISTS => Left(TenantDoesNotExists(tenant)) + case _ => Left(InternalServerError()) + } + } +} + +case class RightValue(name: String, level: RightLevel) + +object userImplicits { + implicit class UserRow(val row: Row) extends AnyVal { + def optRights(): Option[TenantRight] = { + row.optJsObject("rights").flatMap(js => js.asOpt[TenantRight]) + } + + def optRightLevel(field: String): Option[RightLevel] = { + row.optString(field).map(dbRightToRight) + } + + def optUserWithTenantRights(): Option[UserWithTenantRights] = { + for ( + username <- row.optString("username"); + userType <- row.optString("user_type").map(dbUserTypeToUserType); + admin <- row.optBoolean("admin"); + rights <- row.optJsObject("tenants") + ) yield { + val tenantRights = rights.asOpt[Map[String, RightLevel]].getOrElse(Map()) + UserWithTenantRights( + username = username, + email = row.optString("email").orNull, + password = null, + admin = admin, + tenantRights = tenantRights, + userType = userType, + defaultTenant = row.optString("default_tenant") + ) + } + } + + def optUser(): Option[User] = { + for ( + username <- row.optString("username"); + userType <- row.optString("user_type").map(dbUserTypeToUserType); + admin <- row.optBoolean("admin") + ) + yield User( + username = username, + email = row.optString("email").orNull, + password = null, + admin = admin, + userType = userType, + defaultTenant = row.optString("default_tenant") + ) + } + + def optUserWithRights(): Option[UserWithCompleteRightForOneTenant] = { + for ( + username <- row.optString("username"); + userType <- row.optString("user_type").map(dbUserTypeToUserType); + admin <- row.optBoolean("admin") + ) + yield UserWithCompleteRightForOneTenant( + username = username, + email = row.optString("email").orNull, + password = null, + admin = admin, + tenantRight = row.optRights(), + userType = userType, + defaultTenant = row.optString("default_tenant") + ) + } + } + + implicit val rightRead: Reads[RightValue] = { json => + { + for ( + name <- (json \ "name").asOpt[String]; + level <- (json \ "level").asOpt[String] + ) yield { + val right = dbRightToRight(level) + JsSuccess(RightValue(name = name, level = right)) + } + }.getOrElse(JsError("Failed to read rights")) + } + + def dbRightToRight(dbRight: String): RightLevel = { + dbRight match { + case "ADMIN" => RightLevels.Admin + case "READ" => RightLevels.Read + case "WRITE" => RightLevels.Write + } + } + + def dbUserTypeToUserType(userType: String): UserType = { + userType match { + case "OTOROSHI" => OTOROSHI + case "INTERNAL" => INTERNAL + case "OIDC" => OIDC + } + } +} diff --git a/app/fr/maif/izanami/env/Jobs.scala b/app/fr/maif/izanami/env/Jobs.scala new file mode 100644 index 000000000..8cd4c1553 --- /dev/null +++ b/app/fr/maif/izanami/env/Jobs.scala @@ -0,0 +1,18 @@ +package fr.maif.izanami.env + +import fr.maif.izanami.utils.syntax.implicits._ + +import scala.concurrent.{ExecutionContext, Future} + +class Jobs(env: Env) { + + implicit val ec: ExecutionContext = env.executionContext + + def onStart(): Future[Unit] = { + Future.successful(()) + } + + def onStop(): Future[Unit] = { + ().vfuture + } +} diff --git a/app/fr/maif/izanami/env/env.scala b/app/fr/maif/izanami/env/env.scala new file mode 100644 index 000000000..171f7ded0 --- /dev/null +++ b/app/fr/maif/izanami/env/env.scala @@ -0,0 +1,111 @@ +package fr.maif.izanami.env + +import akka.actor.{ActorSystem, Scheduler} +import akka.stream.Materializer +import com.typesafe.config.ConfigFactory +import fr.maif.izanami.datastores._ +import fr.maif.izanami.mail.Mails +import fr.maif.izanami.security.JwtService +import fr.maif.izanami.wasm.IzanamiWasmIntegrationContext +import io.otoroshi.wasm4s.scaladsl.WasmIntegration +import play.api.libs.json.Json +import fr.maif.izanami.wasm.IzanamiWasmIntegrationContext +import io.otoroshi.wasm4s.scaladsl.WasmIntegration +import play.api.libs.ws.WSClient +import play.api.{Configuration, Environment, Logger} + +import javax.crypto.spec.SecretKeySpec +import scala.concurrent._ + +class Datastores(env: Env) { + + private implicit val ec: ExecutionContext = env.executionContext + + val features: FeaturesDatastore = new FeaturesDatastore(env) + val tenants: TenantsDatastore = new TenantsDatastore(env) + val projects: ProjectsDatastore = new ProjectsDatastore(env) + val tags: TagsDatastore = new TagsDatastore(env) + val apiKeys: ApiKeyDatastore = new ApiKeyDatastore(env) + val featureContext: FeatureContextDatastore = new FeatureContextDatastore(env) + val users: UsersDatastore = new UsersDatastore(env) + val configuration: ConfigurationDatastore = new ConfigurationDatastore(env) + val stats: StatsDatastore = new StatsDatastore(env) + + def onStart(): Future[Unit] = { + for { + _ <- users.onStart() + } yield () + } + + def onStop(): Future[Unit] = { + for { + _ <- users.onStop() + } yield () + } +} + +class Env(val configuration: Configuration, val environment: Environment, val Ws: WSClient) { + // TODO variablize with izanami + lazy val wasmCacheTtl: Int = + configuration.getOptional[Int]("app.wasm.cache.ttl").filter(_ >= 5000).getOrElse(60000) + lazy val wasmQueueBufferSize: Int = + configuration.getOptional[Int]("app.wasm.queue.buffer.size").getOrElse(2048) + + val logger = Logger("izanami") + val defaultSecret = configuration.get[String]("app.default-secret") + val secret = configuration.get[String]("app.secret") + + if(defaultSecret == secret) { + logger.warn("You're using Izanami default secret, which is not safe for production. Please generate a new secret and provide it to Izanami.") + } + + lazy val encryptionKey = new SecretKeySpec( + configuration.get[String]("app.authentication.token-body-secret").padTo(16, "0").mkString("").take(16).getBytes, + "AES" + ) + + lazy val expositionUrl = configuration.getOptional[String]("app.exposition.url") + .getOrElse(s"http://localhost:${configuration.getOptional[Int]("http.port").getOrElse(9000)}") + + val actorSystem: ActorSystem = ActorSystem( + "app-actor-system", + configuration + .getOptional[Configuration]("app.actorsystem") + .map(_.underlying) + .getOrElse(ConfigFactory.empty) + ) + + implicit val executionContext: ExecutionContext = actorSystem.dispatcher + val scheduler: Scheduler = actorSystem.scheduler + val materializer: Materializer = Materializer(actorSystem) + + // init subsystems + val postgresql = new Postgresql(this) + val datastores = new Datastores(this) + val mails = new Mails(this) + val jwtService = new JwtService(this) + val wasmIntegration = WasmIntegration(new IzanamiWasmIntegrationContext(this)) + val jobs = new Jobs(this) + + def onStart(): Future[Unit] = { + logger.info(s"Postgres url ${postgresql.getHost}:${postgresql.getPort}") + + for { + _ <- postgresql.onStart() + _ <- datastores.onStart() + _ <- jobs.onStart() + _ <- wasmIntegration.startF() + } yield () + } + + def onStop(): Future[Unit] = { + for { + _ <- wasmIntegration.stopF() + _ <- datastores.onStop() + _ <- postgresql.onStop() + _ <- jobs.onStop() + } yield () + } + + def isDev: Boolean = configuration.getOptional[String]("app.config.mode").exists(mode => mode.equals("dev")) +} diff --git a/app/fr/maif/izanami/env/postgresql.scala b/app/fr/maif/izanami/env/postgresql.scala new file mode 100644 index 000000000..ac6387252 --- /dev/null +++ b/app/fr/maif/izanami/env/postgresql.scala @@ -0,0 +1,381 @@ +package fr.maif.izanami.env + +import akka.http.scaladsl.util.FastFuture +import com.zaxxer.hikari.{HikariConfig, HikariDataSource} +import fr.maif.izanami.datastores.HashUtils +import fr.maif.izanami.security.IdGenerator +import fr.maif.izanami.utils.syntax.implicits.BetterSyntax +import io.vertx.core +import io.vertx.core.Vertx +import io.vertx.core.buffer.Buffer +import io.vertx.core.net.{PemKeyCertOptions, PemTrustOptions} +import io.vertx.pgclient.impl.PgPoolImpl +import io.vertx.pgclient.pubsub.PgSubscriber +import io.vertx.pgclient.{PgConnectOptions, PgPool, SslMode} +import io.vertx.sqlclient.{PoolOptions, Row, RowSet, SqlConnection} +import org.flywaydb.core.Flyway +import play.api.libs.json.{JsArray, JsObject, Json} +import play.api.{Configuration, Logger} + +import java.time.{Instant, OffsetDateTime, ZoneId} +import java.util.UUID +import scala.concurrent.{ExecutionContext, Future, Promise} +import scala.util.{Failure, Success, Try} + +class Postgresql(env: Env) { + + import pgimplicits._ + + import scala.jdk.CollectionConverters._ + + + private val logger = Logger("izanami") + lazy val connectOptions = if (configuration.has("app.pg.uri")) { + logger.info(s"Postgres URI : ${configuration.get[String]("app.pg.uri")}") + val opts = PgConnectOptions.fromUri(configuration.get[String]("app.pg.uri")) + opts + } else { + val ssl = configuration.getOptional[Configuration]("app.pg.ssl").getOrElse(Configuration.empty) + val sslEnabled = ssl.getOptional[Boolean]("enabled").getOrElse(false) + new PgConnectOptions() + .applyOnWithOpt(configuration.getOptional[Int]("connect-timeout"))((p, v) => p.setConnectTimeout(v)) + .applyOnWithOpt(configuration.getOptional[Int]("idle-timeout"))((p, v) => p.setIdleTimeout(v)) + .applyOnWithOpt(configuration.getOptional[Boolean]("log-activity"))((p, v) => p.setLogActivity(v)) + .applyOnWithOpt(configuration.getOptional[Int]("pipelining-limit"))((p, v) => p.setPipeliningLimit(v)) + .setPort(getPort) + .setHost(getHost) + .setDatabase(configuration.getOptional[String]("app.pg.database").getOrElse("postgres")) + .setUser(configuration.getOptional[String]("app.pg.user").getOrElse("postgres")) + .setPassword(configuration.getOptional[String]("app.pg.password").getOrElse("postgres")) + .applyOnIf(sslEnabled) { pgopt => + val mode = SslMode.of(ssl.getOptional[String]("mode").getOrElse("VERIFY_CA")) + val pemTrustOptions = new PemTrustOptions() + val pemKeyCertOptions = new PemKeyCertOptions() + pgopt.setSslMode(mode) + pgopt.applyOnWithOpt(ssl.getOptional[Int]("ssl-handshake-timeout"))((p, v) => p.setSslHandshakeTimeout(v)) + ssl.getOptional[Seq[String]]("trustedCertsPath").map { pathes => + pathes.map(p => pemTrustOptions.addCertPath(p)) + pgopt.setPemTrustOptions(pemTrustOptions) + } + ssl.getOptional[String]("trusted-cert-path").map { path => + pemTrustOptions.addCertPath(path) + pgopt.setPemTrustOptions(pemTrustOptions) + } + ssl.getOptional[Seq[String]]("trusted-certs").map { certs => + certs.map(p => pemTrustOptions.addCertValue(Buffer.buffer(p))) + pgopt.setPemTrustOptions(pemTrustOptions) + } + ssl.getOptional[String]("trusted-cert").map { path => + pemTrustOptions.addCertValue(Buffer.buffer(path)) + pgopt.setPemTrustOptions(pemTrustOptions) + } + ssl.getOptional[Seq[String]]("client-certs-path").map { pathes => + pathes.map(p => pemKeyCertOptions.addCertPath(p)) + pgopt.setPemKeyCertOptions(pemKeyCertOptions) + } + ssl.getOptional[Seq[String]]("client-certs").map { certs => + certs.map(p => pemKeyCertOptions.addCertValue(Buffer.buffer(p))) + pgopt.setPemKeyCertOptions(pemKeyCertOptions) + } + ssl.getOptional[String]("client-cert-path").map { path => + pemKeyCertOptions.addCertPath(path) + pgopt.setPemKeyCertOptions(pemKeyCertOptions) + } + ssl.getOptional[String]("client-cert").map { path => + pemKeyCertOptions.addCertValue(Buffer.buffer(path)) + pgopt.setPemKeyCertOptions(pemKeyCertOptions) + } + ssl.getOptional[Boolean]("trust-all").map { v => + pgopt.setTrustAll(v) + } + pgopt + } + } + lazy val vertx = Vertx.vertx() + private lazy val poolOptions = new PoolOptions() + .setMaxSize(configuration.getOptional[Int]("app.pg.pool-size").getOrElse(100)) + private lazy val pool = PgPool.pool(connectOptions, poolOptions) + + private val configuration = env.configuration + + def getHost = { + configuration.getOptional[String]("app.pg.host").getOrElse("localhost") + } + + def getPort = { + configuration.getOptional[Int]("app.pg.port").getOrElse(5432) + } + + def onStart(): Future[Unit] = { + updateSchema() + Future.successful(()) + } + + def updateSchema(): Unit = { + val config = new HikariConfig() + config.setDriverClassName(classOf[org.postgresql.Driver].getName) + config.setJdbcUrl( + s"jdbc:postgresql://${connectOptions.getHost}:${connectOptions.getPort}/${connectOptions.getDatabase}" + ) + config.setUsername(connectOptions.getUser) + config.setPassword(connectOptions.getPassword) + config.setMaximumPoolSize(10) + val dataSource = new HikariDataSource(config) + val password = defaultPassword + logger.info(s"Password $password") + val flyway = + Flyway.configure + .dataSource(dataSource) + .locations("filesystem:conf/sql/globals", "conf/sql/globals", "sql/globals") + .baselineOnMigrate(true) + .schemas("izanami") + .placeholders(java.util.Map.of("default_admin", "RESERVED_ADMIN_USER", "default_password", HashUtils.bcryptHash(password))) + .load() + val migrationResult = flyway.migrate() + if(migrationResult.initialSchemaVersion == null) { + val isPasswordProvided = configuration.getOptional[String]("app.admin.password").isDefined + if(!isPasswordProvided) { + logger.warn( + s"No password provided in app.admin.password env variable. Therefore password ${password} has been automatically generated for RESERVED_ADMIN_USER account" + ) + } + } + dataSource.close() + } + + def defaultPassword: String = { + val maybeUserProvidedPassword = configuration.getOptional[String]("app.admin.password") + maybeUserProvidedPassword.getOrElse(IdGenerator.token(24)) + } + + def onStop(): Future[Unit] = { + pool.close() + FastFuture.successful(()) + } + + + def updateSearchPath(searchPath: String, conn: SqlConnection): Future[Unit] = { + conn.preparedQuery( + f"SELECT set_config('search_path', $$1, true)" + ) + .execute(io.vertx.sqlclient.Tuple.of(searchPath)).mapEmpty().scala + } + + private def setSearchPath(schemas: Set[String], conn: SqlConnection): io.vertx.core.Future[RowSet[Row]] = { + if (schemas.nonEmpty) { + conn.preparedQuery(f"SELECT set_config('search_path', $$1, true)").execute(io.vertx.sqlclient.Tuple.of(schemas.mkString(","))) + } else { + io.vertx.core.Future.succeededFuture() + } + } + + def executeInTransaction[T](callback: SqlConnection => Future[T], schemas: Set[String] = Set()): Future[T] = { + var future: io.vertx.core.Future[T] = io.vertx.core.Future.succeededFuture() + pool + .withTransaction(conn => { + var searchPathFuture = setSearchPath(schemas, conn) + future = searchPathFuture.flatMap(_ => callback(conn).vertx(env.executionContext)) + future + }) + .recover(err => { + logger.error("Failed to execute queries in transaction", err) + future + }) + .scala // Bubble up query error instead of TransactionRollbackException that does not carry much information + } + + def queryAll[A]( + query: String, + params: List[AnyRef] = List.empty, + debug: Boolean = false, + schemas: Set[String] = Set(), + conn: Option[SqlConnection] = None + )( + f: Row => Option[A] + ): Future[List[A]] = { + queryRaw[List[A]](query, params, debug, schemas, conn)(rows => rows.map(f).flatten.toList) + } + + def queryAllOpt[A]( + query: String, + params: List[AnyRef] = List.empty, + debug: Boolean = false, + schemas: Set[String] = Set(), + conn: Option[SqlConnection] = None + )( + f: Row => Option[A] + ): Future[List[Option[A]]] = { + queryRaw[List[Option[A]]](query, params, debug, schemas, conn)(rows => rows.map(f).toList) + } + + def queryRaw[A]( + query: String, + params: List[AnyRef] = List.empty, + debug: Boolean = false, + schemas: Set[String] = Set(), + conn: Option[SqlConnection] = None + )( + f: List[Row] => A + ): Future[A] = { + if (debug) env.logger.info(s"""query: "$query", params: "${params.mkString(", ")}"""") + val isRead = query.toLowerCase().trim.startsWith("select") + (isRead match { + case true => + val lambda = (c: SqlConnection) => { + c.preparedQuery(query).execute(io.vertx.sqlclient.Tuple.from(params.toArray)) + } + conn + .map(conn => setSearchPath(schemas, conn).flatMap(_ => lambda(conn))) + .map(f => f.scala) + .getOrElse(executeInTransaction(lambda(_).scala, schemas)) + case false => + conn + .map(c => setSearchPath(schemas, c).flatMap(_ => c.preparedQuery(query).execute(io.vertx.sqlclient.Tuple.from(params.toArray))).scala) + .getOrElse(executeInTransaction(conn => conn.preparedQuery(query).execute(io.vertx.sqlclient.Tuple.from(params.toArray)).scala, schemas)) + }).flatMap { _rows => + Try { + val rows = _rows.asScala.toList + f(rows) + } match { + case Success(value) => FastFuture.successful(value) + case Failure(e) => FastFuture.failed(e) + } + }(env.executionContext) + .andThen { case Failure(e) => + logger.error(s"""Failed to apply query: "$query" with params: "${params.mkString(", ")}"""", e) + }(env.executionContext) + } + + def queryOne[A]( + query: String, + params: List[AnyRef] = List.empty, + debug: Boolean = false, + schemas: Set[String] = Set(), + conn: Option[SqlConnection] = None + )( + f: Row => Option[A] + ): Future[Option[A]] = { + queryRaw[Option[A]](query, params, debug, schemas, conn)(rows => rows.headOption.flatMap(row => f(row))) + } + +} + +object PostgresqlErrors { + val UNIQUE_VIOLATION = "23505" + val INTEGRITY_CONSTRAINT_VIOLATION = "23000" + val NOT_NULL_VIOLATION = "23502" + val FOREIGN_KEY_VIOLATION = "23503" + val CHECK_VIOLATION = "23514" + val RELATION_DOES_NOT_EXISTS = "42P01" +} + +object pgimplicits { + implicit class VertxFutureEnhancer[A](val future: io.vertx.core.Future[A]) extends AnyVal { + def scala: Future[A] = { + val promise = Promise.apply[A]() + future.onSuccess(a => promise.trySuccess(a)) + future.onFailure { e => + promise.tryFailure(e) + } + promise.future + } + } + + implicit class ScalaFutureEnhancer[A](val future: Future[A]) extends AnyVal { + def vertx(implicit ec: ExecutionContext): io.vertx.core.Future[A] = { + val promise = io.vertx.core.Promise.promise[A]() + future.onComplete { + case Failure(err) => promise.fail(err) + case Success(value) => promise.complete(value) + } + + promise.future + } + } + + implicit class VertxQueryEnhancer[A](val query: io.vertx.sqlclient.Query[A]) extends AnyVal { + def executeAsync(): Future[A] = { + val promise = Promise.apply[A]() + val future = query.execute() + future.onSuccess(a => promise.trySuccess(a)) + future.onFailure { e => + promise.tryFailure(e) + } + promise.future + } + } + + implicit class VertxPreparedQueryEnhancer[A](val query: io.vertx.sqlclient.PreparedQuery[A]) extends AnyVal { + def executeAsync(): Future[A] = { + val promise = Promise.apply[A]() + val future = query.execute() + future.onSuccess(a => promise.trySuccess(a)) + future.onFailure { e => + promise.tryFailure(e) + } + promise.future + } + } + + implicit class EnhancedRow(val row: Row) extends AnyVal { + def optString(name: String): Option[String] = opt(name, "String", (a, b) => a.getString(b)) + + def optStringArray(name: String): Option[Array[String]] = opt(name, "String", (a, b) => a.getArrayOfStrings(b)) + + def optUUID(name: String): Option[UUID] = opt(name, "UUID", (a, b) => a.getUUID(b)) + + def opt[A](name: String, typ: String, extractor: (Row, String) => A): Option[A] = { + Try(extractor(row, name)) match { + case Failure(ex) => { + //logger.error(s"error while getting column '$name' of type $typ", ex) + None + } + case Success(value) => Option(value) + } + } + + def optDouble(name: String): Option[Double] = opt(name, "Double", (a, b) => a.getDouble(b).doubleValue()) + def optInt(name: String): Option[Int] = opt(name, "Integer", (a, b) => a.getDouble(b).intValue()) + def optBoolean(name: String): Option[Boolean] = opt(name, "Boolean", (a, b) => a.getBoolean(b)) + def optLong(name: String): Option[Long] = + opt(name, "Long", (a, b) => a.getLong(b).longValue()) + + def optDateTime(name: String): Option[OffsetDateTime] = { + optOffsetDatetime(name).map { d => + val id = if (d.getOffset.getId == "Z") "UTC" else d.getOffset.getId + val instant = Instant.ofEpochMilli(d.toInstant.toEpochMilli) + OffsetDateTime.ofInstant(instant, ZoneId.of(id)) + } + } + + def optOffsetDatetime(name: String): Option[OffsetDateTime] = + opt(name, "OffsetDateTime", (a, b) => a.getOffsetDateTime(b)) + + def optJsObject(name: String): Option[JsObject] = + opt( + name, + "JsObject", + (row, _) => { + Try { + Json.parse(row.getJsonObject(name).encode()).as[JsObject] + } match { + case Success(s) => s + case Failure(e) => Json.parse(row.getString(name)).as[JsObject] + } + } + ) + def optJsArray(name: String): Option[JsArray] = + opt( + name, + "JsArray", + (row, _) => { + Try { + Json.parse(row.getJsonArray(name).encode()).as[JsArray] + } match { + case Success(s) => s + case Failure(e) => Json.parse(row.getString(name)).as[JsArray] + } + } + ) + } +} diff --git a/app/fr/maif/izanami/errors/Errors.scala b/app/fr/maif/izanami/errors/Errors.scala new file mode 100644 index 000000000..ebb636a7b --- /dev/null +++ b/app/fr/maif/izanami/errors/Errors.scala @@ -0,0 +1,72 @@ +package fr.maif.izanami.errors + +import play.api.http.Status.{BAD_REQUEST, FORBIDDEN, INTERNAL_SERVER_ERROR, NOT_FOUND, UNAUTHORIZED} +import play.api.libs.json.{Json, Writes} +import play.api.mvc.{Result, Results} + +import java.util.Objects +import scala.collection.immutable.Iterable + +sealed abstract class IzanamiError(val message: String, val status: Int) { + // TODO rework controllers to use this + def toHttpResponse: Result = Results.Status(status)(Json.obj("message" -> message)) +} +case class TenantAlreadyExists(name: String) + extends IzanamiError(message = s"Tenant ${name} already exists", status = BAD_REQUEST) +case class ProjectAlreadyExists(name: String, tenant: String) + extends IzanamiError(message = s"Project ${name} already exists in tenant ${tenant}", status = BAD_REQUEST) +case class TenantDoesNotExists(id: String) + extends IzanamiError(message = s"Tenant ${id} does not exist", status = NOT_FOUND) +case class TagDoesNotExists(id: String) + extends IzanamiError(message = s"Tag ${id} does not exist in tenant", status = NOT_FOUND) +case class OneTagDoesNotExists(names: Set[String]) + extends IzanamiError(message = s"""One or more of the following tags does not exist : [${names.map(n => s""""${n}"""").mkString(",")}]""", status = BAD_REQUEST) +case class ProjectDoesNotExists(id: String) + extends IzanamiError(message = s"Project ${id} does not exist", status = NOT_FOUND) +case class ProjectOrFeatureDoesNotExists(project: String, feature: String) + extends IzanamiError(message = s"Project ${project} or feature ${feature} does not exist", status = NOT_FOUND) +case class OneProjectDoesNotExists(names: Iterable[String]) + extends IzanamiError(message = s"""One or more of the following project does not exist : [${names.map(n => s""""${n}"""").mkString(",")}]""", status = BAD_REQUEST) +case class MissingFeatureFields() + extends IzanamiError(message = "Some fields are missing for feature object", status = BAD_REQUEST) +case class FeatureNotFound(id: String) + extends IzanamiError(message = s"Feature ${id} does not exists", status = NOT_FOUND) +case class KeyNotFound(name: String) + extends IzanamiError(message = s"Key ${name} does not exists", status = NOT_FOUND) +case class ProjectContextOrFeatureDoesNotExist(project: String, context: String, feature: String) + extends IzanamiError(message = s"Project ${project}, context ${context} or feature ${feature} does not exist", status = NOT_FOUND) +case class FeatureContextDoesNotExist(context: String) + extends IzanamiError(message = s"Context ${context} does not exist", status = NOT_FOUND) + +case class ConflictWithSameNameGlobalContext(name: String, parentCtx: String = null) extends IzanamiError(message=s"A global context with this name ($name) already exist ${if( Objects.nonNull(parentCtx)) s"as child of $parentCtx" else "at root level"}", status=BAD_REQUEST) +case class ConflictWithSameNameLocalContext(name: String, parentCtx: String = null) extends IzanamiError(message=s"A local context with this name ($name) already exist ${if( Objects.nonNull(parentCtx)) s"as child of $parentCtx" else "at root level"}", status=BAD_REQUEST) + +case class UserNotFound(user: String) extends IzanamiError(message = s"User ${user} does not exist", status = NOT_FOUND) +case class SessionNotFound(session: String) extends IzanamiError(message = s"Session ${session} does not exist", status = UNAUTHORIZED) +case class UserAlreadyExist(user: String, email: String) extends IzanamiError(message = s"User ${user} already exists (or email ${email} is already used by another user)", status = BAD_REQUEST) +case class EmailAlreadyUsed(email: String) extends IzanamiError(message = s"Email ${email} is already used by another user)", status = BAD_REQUEST) +case class InternalServerError(msg: String = "") extends IzanamiError(message = s"Something went wrong $msg", status = INTERNAL_SERVER_ERROR) +case class MailSendingError(err: String, override val status: Int = INTERNAL_SERVER_ERROR) extends IzanamiError(message = s"Failed to send mail : ${err}", status) +case class ConfigurationReadError() extends IzanamiError(message = s"Failed to read configuration from DB", status=INTERNAL_SERVER_ERROR) +case class MissingMailProviderConfigurationError(mailer: String) extends IzanamiError(message= s"Missing configuration for mail provider ${mailer}", status=BAD_REQUEST) +case class BadBodyFormat() extends IzanamiError(message= "Bad body format", status = BAD_REQUEST) +case class NotEnoughRights() extends IzanamiError(message= "Not enough rights for this operation", status = FORBIDDEN) +case class InvalidCredentials() extends IzanamiError(message= "Incorrect username / password", status = FORBIDDEN) +case class FeatureOverloadDoesNotExist(project: String, path: String, feature: String) extends IzanamiError(message= s"No overload for feature ${feature} found at ${path} (project ${project})", status = NOT_FOUND) +case class WasmScriptAlreadyExists(path: String) extends IzanamiError(message=s"Script ${path} already exists", status = BAD_REQUEST) +case class FeatureDependsOnThisScript() extends IzanamiError(message=s"Can't delete a script used by existing features", status = BAD_REQUEST) +case class ApiKeyDoesNotExist(name: String) extends IzanamiError(message=s"Key ${name} does not exist", status = NOT_FOUND) +case class FeatureDoesNotExist(name: String) extends IzanamiError(message=s"Feature ${name} does not exist", status = NOT_FOUND) +case class NoWasmManagerConfigured() extends IzanamiError(message=s"No wasm manager is configured, can't handle wasm scripts", status = BAD_REQUEST) +case class FailedToReadEvent(event: String) extends IzanamiError(message=s"Failed to read event $event", status = INTERNAL_SERVER_ERROR) +case class MissingOIDCConfigurationError() extends IzanamiError(message=s"OIDC configuration is either missing or incomplete", status = INTERNAL_SERVER_ERROR) +case class WasmError() extends IzanamiError(message="Failed to parse wasm response", status = INTERNAL_SERVER_ERROR) +case class MissingProjectRight(projects: Set[String]) extends IzanamiError(message=s"""You're not allowed for projects ${projects.mkString(",")}""", status = FORBIDDEN) +case class MissingFeatureRight(features: Set[String]) extends IzanamiError(message=s"""You're not allowed for features ${features.mkString(",")}, you don't have right for this project""", status = FORBIDDEN) +object IzanamiError { + implicit val errorWrite: Writes[IzanamiError] = { err => + Json.obj( + "message" -> err.message + ) + } +} diff --git a/app/fr/maif/izanami/errors/IzanamiHttpErrorHandler.scala b/app/fr/maif/izanami/errors/IzanamiHttpErrorHandler.scala new file mode 100644 index 000000000..e3e0f72fd --- /dev/null +++ b/app/fr/maif/izanami/errors/IzanamiHttpErrorHandler.scala @@ -0,0 +1,41 @@ +package fr.maif.izanami.errors + +import fr.maif.izanami.env.Env +import play.api.{Logger, mvc} +import play.api.http.Status.INTERNAL_SERVER_ERROR +import play.api.libs.json.Json +import play.api.mvc.Result +import play.api.mvc.Results.Status +import play.api.http.HttpErrorHandler + +import java.security.SecureRandom +import scala.concurrent.{ExecutionContext, Future} + +class IzanamiHttpErrorHandler(env: Env) extends HttpErrorHandler { + + implicit val ec: ExecutionContext = env.executionContext + + lazy val logger = Logger("izanami-error-handler") + + override def onClientError(request: mvc.RequestHeader, statusCode: Int, message: String): Future[Result] = { + val uuid = + java.util.UUID.nameUUIDFromBytes(new SecureRandom().generateSeed(16)) + val msg = + Option(message).filterNot(_.trim.isEmpty).getOrElse("An error occured") + val errorMessage = + s"Client Error [$uuid]: $msg on ${request.uri} ($statusCode)" + + logger.error(errorMessage) + Future.successful(Status(statusCode)(Json.obj("message" -> msg)).withHeaders(("content-type", "application/json"))) + } + + override def onServerError(request: mvc.RequestHeader, exception: Throwable): Future[Result] = { + val uuid = + java.util.UUID.nameUUIDFromBytes(new SecureRandom().generateSeed(16)) + + logger.error( + s"Server Error [$uuid]: ${exception.getMessage} on ${request.uri}", + exception) + Future.successful(Status(INTERNAL_SERVER_ERROR)(Json.obj("message" -> exception.getMessage)).withHeaders(("content-type", "application/json"))) + } +} diff --git a/app/fr/maif/izanami/mail/MailFactory.scala b/app/fr/maif/izanami/mail/MailFactory.scala new file mode 100644 index 000000000..556e2b8ea --- /dev/null +++ b/app/fr/maif/izanami/mail/MailFactory.scala @@ -0,0 +1,51 @@ +package fr.maif.izanami.mail + +import fr.maif.izanami.env.Env + +class MailFactory(env: Env) { + def invitationEmail(target: String, token: String): Mail = { + val baseUrl = env.expositionUrl + val completeUrl = s"${baseUrl}/invitation?token=${token}" + Mail( + subject = "You've been invited to Izanami", + targetMail = target, + textContent = + s""" + |You've been invited to Izanami. + |Click on this link to finalize your account creation : ${baseUrl}?invitation=${token} + | + |If you don't know what it's about, you can safely ignore this mail. + |""".stripMargin, + htmlContent = + s""" + |You've been invited to Izanami. + |Click here to finalize your account creation. + | + |If you don't know what it's about, you can safely ignore this mail. + |""".stripMargin + ) + } + + def passwordResetEmail(target: String, token: String): Mail = { + val baseUrl = env.expositionUrl + val completeUrl = s"${baseUrl}/password/_reset?token=${token}" + Mail( + subject = "Izanami password reset", + targetMail = target, + textContent = + s""" + |A password reset request has been made for your account. + |Click on this link to reset your password : ${completeUrl} + | + |If you you didn't ask to reset your password, you can safely ignore this mail. + |""".stripMargin, + htmlContent = + s""" + |A password reset request has been made for your account. + |Click here to reset your password. + | + |If you you didn't ask to reset your password, you can safely ignore this mail. + |""".stripMargin + ) + } +} diff --git a/app/fr/maif/izanami/mail/Mails.scala b/app/fr/maif/izanami/mail/Mails.scala new file mode 100644 index 000000000..a50e05e2e --- /dev/null +++ b/app/fr/maif/izanami/mail/Mails.scala @@ -0,0 +1,232 @@ +package fr.maif.izanami.mail + +import com.mailjet.client.resource.Emailv31 +import com.mailjet.client.{ClientOptions, MailjetClient, MailjetRequest} +import com.sun.mail.smtp.SMTPTransport +import fr.maif.izanami.env.Env +import fr.maif.izanami.errors.{IzanamiError, MailSendingError, MissingMailProviderConfigurationError} +import fr.maif.izanami.mail.MailGunRegions.{Europe, MailGunRegion} +import fr.maif.izanami.mail.MailerTypes.MailerType +import fr.maif.izanami.utils.syntax.implicits.BetterSyntax +import org.json.{JSONArray, JSONObject} +import play.api.Logger +import play.api.libs.ws.{WSAuthScheme, WSClient} + +import java.util.{Objects, Properties} +import javax.mail.internet.{InternetAddress, MimeMessage} +import javax.mail.{Message, Session} +import scala.concurrent.{ExecutionContext, Future} +import scala.jdk.FutureConverters.CompletionStageOps +import scala.util.Using + +case class Mail(subject: String, targetMail: String, textContent: String = "", htmlContent: String = "") + +sealed trait MailProviderConfiguration { + val mailerType: MailerType +} + +object ConsoleMailProvider extends MailProviderConfiguration { + val mailerType: MailerType = MailerTypes.Console +} + +case class MailJetConfiguration(apiKey: String, secret: String, url: Option[String] = None) + +case class SMTPConfiguration( + host: String, + port: Option[Int] = None, + user: Option[String] = None, + password: Option[String] = None, + auth: Boolean = false, + starttlsEnabled: Boolean = false, + smtps: Boolean = true +) + +case class SMTPMailProvider(configuration: SMTPConfiguration) extends MailProviderConfiguration { + val mailerType: MailerType = MailerTypes.SMTP +} + +case class MailJetMailProvider(configuration: MailJetConfiguration) extends MailProviderConfiguration { + val mailerType: MailerType = MailerTypes.MailJet +} + +object MailGunRegions extends Enumeration { + type MailGunRegion = Value + val Europe, US = Value +} + +case class MailGunConfiguration( + apiKey: String, + url: Option[String], + region: MailGunRegion = Europe +) + +case class MailGunMailProvider(configuration: MailGunConfiguration) extends MailProviderConfiguration { + val mailerType: MailerType = MailerTypes.MailGun +} + +class Mails(env: Env) { + private val mailFactory = new MailFactory(env) + private implicit val ec: ExecutionContext = env.executionContext + + def sendMail(mail: Mail): Future[Either[IzanamiError, Unit]] = { + env.datastores.configuration + .readFullConfiguration() + .flatMap(eitherConfiguration => { + eitherConfiguration.fold( + err => Left(err).future, + configuration => { + configuration.mailConfiguration match { + case ConsoleMailProvider => ConsoleMailService.sendMail(mail) + case MailJetMailProvider(MailJetConfiguration(apiKey, secret, _)) + if Objects.isNull(apiKey) || Objects.isNull(secret) => + Left(MissingMailProviderConfigurationError("MailJet")).future + case MailJetMailProvider(conf) => MailJetService.sendMail(mail, conf, configuration.originEmail.get) + case MailGunMailProvider(configuration) if Objects.isNull(configuration.apiKey) => + Left(MissingMailProviderConfigurationError("MailJet")).future + case MailGunMailProvider(mailConf) => + MailGunService.sendMail(mail, mailConf, configuration.originEmail.get, env.Ws) + case SMTPMailProvider(mailConf) => + SMTPMailService.sendMail(mail, mailConf, configuration.originEmail.get) + } + } + ) + }) + } + + def sendInvitationMail(targetAdress: String, token: String): Future[Either[IzanamiError, Unit]] = + sendMail(mailFactory.invitationEmail(targetAdress, token)) + + def sendPasswordResetEmail(targetAdress: String, token: String): Future[Either[IzanamiError, Unit]] = + sendMail(mailFactory.passwordResetEmail(targetAdress, token)) +} + +object MailerTypes extends Enumeration { + type MailerType = Value + val MailJet, Console, MailGun, SMTP = Value +} + +object MailGunService { + val US_URL = "https://api.mailgun.net/v3" + val EUROPE_URL = "https://api.eu.mailgun.net/v3" + + def sendMail(mail: Mail, mailerConfiguration: MailGunConfiguration, originEmail: String, ws: WSClient)(implicit + ec: ExecutionContext + ): Future[Either[MailSendingError, Unit]] = { + val domain = originEmail.split("@")(1) + val url = mailerConfiguration.url.getOrElse(if (mailerConfiguration.region == Europe) EUROPE_URL else US_URL) + val request = ws + .url(s"${url}/${domain}/messages") + .withAuth("api", mailerConfiguration.apiKey, WSAuthScheme.BASIC) + request + .post( + Map( + "from" -> s"""Izanami ${originEmail}""", + "to" -> s"""${mail.targetMail}""", + "subject" -> "You've been invited to Izanami", + "html" -> mail.htmlContent + ) + ) + .map { + case response if response.status >= 400 => Left(MailSendingError(response.body, response.status)) + case _ => Right(()) + + } + } +} + +object MailJetService { + def sendMail(mail: Mail, mailerConfiguration: MailJetConfiguration, originEmail: String)(implicit + ec: ExecutionContext + ): Future[Either[MailSendingError, Unit]] = { + val clientBuilder = ClientOptions.builder() + mailerConfiguration.url.foreach(url => clientBuilder.baseUrl(url)) + val client = new MailjetClient( + clientBuilder + .apiKey(mailerConfiguration.apiKey) + .apiSecretKey(mailerConfiguration.secret) + .build() + ); + val request = new MailjetRequest(Emailv31.resource) + .property( + Emailv31.MESSAGES, + new JSONArray() + .put( + new JSONObject() + .put( + Emailv31.Message.FROM, + new JSONObject() + .put("Email", originEmail) + .put("Name", "Izanami") + ) + .put( + Emailv31.Message.TO, + new JSONArray() + .put( + new JSONObject() + .put("Email", mail.targetMail) + ) + ) + .put(Emailv31.Message.SUBJECT, mail.subject) + .put(Emailv31.Message.TEXTPART, mail.textContent) + .put(Emailv31.Message.HTMLPART, mail.htmlContent) + ) + ) + client + .postAsync(request) + .asScala + .map(response => { + if (response.getStatus > 400) { + Left(MailSendingError(response.toString, response.getStatus)) + } else { + Right(()) + } + })(ec) + } +} + +object SMTPMailService { + def sendMail(mail: Mail, configuration: SMTPConfiguration, originEmail: String)(implicit + ec: ExecutionContext + ): Future[Either[MailSendingError, Unit]] = { + val props = new Properties() + val protocol = if (configuration.smtps) "smtps" else "smtp" + props.put(s"mail.${protocol}.host", configuration.host) + configuration.port.map(port => props.put(s"mail.${protocol}.port", port)) + props.put(s"mail.${protocol}.starttls.enable", configuration.starttlsEnabled) + props.put(s"mail.${protocol}.auth", configuration.auth) + + val session = Session.getInstance(props, null) + val msg = new MimeMessage(session) + msg.setFrom(new InternetAddress(originEmail)) + msg.addRecipient(Message.RecipientType.TO, new InternetAddress(mail.targetMail)) + msg.setSubject("Izanami") + msg.setContent(mail.htmlContent, "text/html; charset=utf-8") + + Future { + Using(session.getTransport(protocol).asInstanceOf[SMTPTransport]) { transport => + { + transport.connect(configuration.host, configuration.user.getOrElse(originEmail), configuration.password.orNull) + transport.sendMessage(msg, msg.getAllRecipients) + } + }.toEither.left.map(t => { + println(t.getMessage) + MailSendingError(t.getMessage, 500) + }) + } + } +} + +object ConsoleMailService { + private val logger: Logger = Logger("izanami-mailer") + def sendMail( + mail: Mail + ): Future[Either[MailSendingError, Unit]] = { + logger.info(s""" + |To: ${mail.targetMail} + |Subject: ${mail.subject} + |Text content: ${mail.textContent} + |Html content: ${mail.htmlContent} + |""".stripMargin) + Future.successful(Right(())) + } +} diff --git a/app/fr/maif/izanami/models/ApiKeys.scala b/app/fr/maif/izanami/models/ApiKeys.scala new file mode 100644 index 000000000..79d617a29 --- /dev/null +++ b/app/fr/maif/izanami/models/ApiKeys.scala @@ -0,0 +1,52 @@ +package fr.maif.izanami.models + +import fr.maif.izanami.security.IdGenerator +import fr.maif.izanami.security.IdGenerator.token +import play.api.libs.functional.syntax.toFunctionalBuilderOps +import play.api.libs.json._ + +import java.util.UUID + +case class ApiKeyProject(name: String, id: UUID) + +case class ApiKeyWithCompleteRights(tenant: String, clientId: String=null, clientSecret: String = null, name: String, projects: Set[ApiKeyProject] = Set(), enabled: Boolean, legacy: Boolean, admin: Boolean) + +case class ApiKey(tenant: String, clientId: String=null, clientSecret: String=null, name: String, projects: Set[String] = Set(), description: String = "", enabled: Boolean = true, legacy: Boolean = false, admin: Boolean = false) { + def withNewSecret(): ApiKey = { + copy(clientSecret = token(64)) + } + def withNewClientId(): ApiKey = { + copy(clientId = IdGenerator.namedToken(tenant, 16)) + } +} + +object ApiKey { + def read(json: JsValue, tenant: String): JsResult[ApiKey] = ( + (__ \ "clientId").readNullable[String] and + (__ \ "name").read[String](Reads.pattern(NAME_REGEXP, s"Name does not match regexp ${NAME_REGEXP.pattern}")) and + (__ \ "description").readNullable[String] and + (__ \ "enabled").read[Boolean] and + (__ \ "projects").readWithDefault[Set[String]](Set())(Reads.set(Reads.pattern(NAME_REGEXP, s"Name does not match regexp ${NAME_REGEXP.pattern}"))) and + (__ \ "admin").readWithDefault(false) + )((clientId, name, description, enabled, projects, admin) => ApiKey(clientId = clientId.orNull, name=name, tenant=tenant, projects = projects, description=description.getOrElse(""), enabled=enabled, admin=admin)).reads(json) + + implicit val keyWrites: Writes[ApiKey] = { key => + Json.obj( + "clientId" -> key.clientId, + "clientSecret" -> key.clientSecret, + "name" -> key.name, + "description" -> key.description, + "projects" -> key.projects, + "enabled" -> key.enabled, + "admin" -> key.admin + ) + } + + def extractTenant(clientId: String): Option[String] = { + if(!clientId.contains("_")) { + None + } else { + clientId.split("_", 2).headOption.filter(_.nonEmpty) + } + } +} diff --git a/app/fr/maif/izanami/models/FeatureContext.scala b/app/fr/maif/izanami/models/FeatureContext.scala new file mode 100644 index 000000000..e9fd38c17 --- /dev/null +++ b/app/fr/maif/izanami/models/FeatureContext.scala @@ -0,0 +1,95 @@ +package fr.maif.izanami.models + +import fr.maif.izanami.env.Env +import fr.maif.izanami.errors.IzanamiError +import fr.maif.izanami.models.Feature.activationConditionRead +import fr.maif.izanami.utils.syntax.implicits.BetterSyntax +import fr.maif.izanami.wasm.{WasmConfig, WasmUtils} +import play.api.libs.json._ + +import scala.concurrent.{ExecutionContext, Future} +import scala.util.matching.Regex + +case class FeatureContext(id: String, name: String, parent: String = null, children: Seq[FeatureContext]= Seq(), overloads: Seq[AbstractFeature] = Seq(), global: Boolean, project: Option[String] = None) + +sealed trait ContextualFeatureStrategy{ + val enabled: Boolean + val feature: String + def active(requestContext: RequestContext, env: Env): Future[Either[IzanamiError, Boolean]] +} + + +case class ClassicalFeatureStrategy( + enabled: Boolean, + conditions: Set[ActivationCondition], + feature: String + ) extends ContextualFeatureStrategy { + def active(requestContext: RequestContext, env: Env): Future[Either[IzanamiError, Boolean]] = { + // TODO handle default as for features + Future(Right{ + enabled && (conditions.isEmpty || conditions.exists(cond => cond.active(requestContext, feature))) + })(env.executionContext) + } +} + +case class WasmFeatureStrategy( + enabled: Boolean, + wasmConfig: WasmConfig, + feature: String +) extends ContextualFeatureStrategy { + def active(requestContext: RequestContext, env: Env): Future[Either[IzanamiError, Boolean]] = { + if(!enabled) { + Future {Right(false)}(env.executionContext) + } else { + WasmUtils.handle(wasmConfig, requestContext)(env.executionContext, env) + } + } +} + +object FeatureContext { + def generateSubContextId(project: String, name: String, path: Seq[String] = Seq()): String = + generateSubContextId(project, path.appended(name)) + + def generateSubContextId(project: String, path: Seq[String]): String = + s"${(project +: path).mkString("_")}" + + def genratePossibleIds(project: String, path: Seq[String] = Seq()): Seq[String] = { + path + .foldLeft(Seq():Seq[String])((acc, next) => acc.prepended(acc.headOption.map(last => s"${last}_${next}").getOrElse(next))) + .map(keyEnd => s"${project}_${keyEnd}") + } + + def readcontextualFeatureStrategyRead(json: JsValue, feature: String): JsResult[ContextualFeatureStrategy] = { + val maybeConditions = (json \ "conditions").asOpt[Set[ActivationCondition]] + val maybeWasmConfig = (json \ "wasmConfig").asOpt[WasmConfig](WasmConfig.format) + val enabled = (json \ "enabled").asOpt[Boolean].getOrElse(true) + + (maybeWasmConfig, maybeConditions) match { + case (Some(config), _) => JsSuccess(WasmFeatureStrategy(enabled, config, feature)) + case (_, maybeConditions) => JsSuccess(ClassicalFeatureStrategy(enabled, maybeConditions.getOrElse(Set(ActivationCondition())), feature)) + } + } + + implicit val featureContextWrites: Writes[FeatureContext] = { context => + Json.obj( + "name" -> context.name, + "id" -> context.id, + "overloads" -> {context.overloads.map(f => Feature.featureWrite.writes(f))}, + "children" -> {context.children.map(f => FeatureContext.featureContextWrites.writes(f))}, + "global" -> context.global, + "project" -> context.project + ) + } + + val CONTEXT_REGEXP: Regex = "^[a-zA-Z0-9-]+$".r + def readFeatureContext(json: JsValue, global: Boolean): JsResult[FeatureContext] = { + val name = (json \ "name").asOpt[String].filter(id => CONTEXT_REGEXP.pattern.matcher(id).matches()) + val id = (json \ "id").asOpt[String] + + name + .map(n => FeatureContext(id.orNull, n, global = global)) // TODO CHANGEME + .map(JsSuccess(_)) + .getOrElse(JsError("Error reading context")) + + } +} diff --git a/app/fr/maif/izanami/models/Features.scala b/app/fr/maif/izanami/models/Features.scala new file mode 100644 index 000000000..e1ae6948f --- /dev/null +++ b/app/fr/maif/izanami/models/Features.scala @@ -0,0 +1,776 @@ +package fr.maif.izanami.models + +import fr.maif.izanami.env.Env +import fr.maif.izanami.errors.IzanamiError +import fr.maif.izanami.utils.syntax.implicits.{BetterJsValue, BetterSyntax} +import fr.maif.izanami.v1.{OldFeature, OldGlobalScriptFeature, OldScript} +import fr.maif.izanami.v1.OldFeature.{oldFeatureReads, oldFeatureWrites} +import fr.maif.izanami.wasm.{WasmConfig, WasmUtils} +import fr.maif.izanami.web.FeatureContextPath +import play.api.libs.json.Reads.instantReads +import play.api.libs.json._ +import play.api.mvc.QueryStringBindable + +import java.time._ +import java.time.format.{DateTimeFormatter, DateTimeParseException} +import java.util.UUID +import scala.concurrent.{ExecutionContext, Future} +import scala.util.hashing.MurmurHash3 +import scala.util.matching.Regex +import scala.util.{Failure, Success, Try} + +sealed trait PatchOperation +case class PatchPath(id: String, path: PatchPathField) {} +sealed trait PatchPathField + +case object Replace extends PatchOperation +case object Remove extends PatchOperation + +case object Enabled extends PatchPathField +case object RootFeature extends PatchPathField + + +sealed trait FeaturePatch { + def op: PatchOperation + def path: PatchPathField + def id: String +} + +case class EnabledFeaturePatch(value: Boolean, id: String) extends FeaturePatch { + override def op: PatchOperation = Replace + override def path: PatchPathField = Enabled +} + +case class RemoveFeaturePatch(id: String) extends FeaturePatch { + override def op: PatchOperation = Remove + override def path: PatchPathField = RootFeature +} + +object FeaturePatch { + val ENABLED_PATH_PATTERN: Regex = "^/(?\\S+)/enabled$".r + val FEATURE_PATH_PATTERN: Regex = "^/(?\\S+)$".r + + implicit val patchPathReads: Reads[PatchPath] = Reads[PatchPath] { json => + json + .asOpt[String] + .map { case ENABLED_PATH_PATTERN(id) => + PatchPath(id, Enabled) + case FEATURE_PATH_PATTERN(id) => PatchPath(id, RootFeature) + } + .map(path => JsSuccess(path)) + .getOrElse(JsError("Bad patch path")) + } + + implicit val patchOpReads: Reads[PatchOperation] = Reads[PatchOperation] { json => + json + .asOpt[String] + .map { + case "replace" => Replace + case "remove" => Remove + } + .map(op => JsSuccess(op)) + .getOrElse(JsError("Bad patch operation")) + } + + implicit val featurePatchReads: Reads[FeaturePatch] = Reads[FeaturePatch] { json => + val maybeResult = for ( + op <- (json \ "op").asOpt[PatchOperation]; + path <- (json \ "path").asOpt[PatchPath] + ) yield (op, path) match { + case (Replace, PatchPath(id, Enabled)) => (json \ "value").asOpt[Boolean].map(b => EnabledFeaturePatch(b, id)) + case (Remove, PatchPath(id, RootFeature)) => Some(RemoveFeaturePatch(id)) + case (_,_) => None + } + maybeResult.flatten.map(r => JsSuccess(r)).getOrElse(JsError("Failed to read patch operation")) + } +} + + + + +case class FeaturePeriod( + begin: Option[Instant] = None, + end: Option[Instant] = None, + hourPeriods: Set[HourPeriod] = Set(), + days: Option[ActivationDayOfWeeks] = None, + timezone: ZoneId = ZoneId.systemDefault() +) { + def active(context: RequestContext): Boolean = { + val now = context.now + begin.forall(i => i.isBefore(now)) && + end.forall(i => i.isAfter(now)) && + (hourPeriods.isEmpty || hourPeriods.exists(_.active(context, timezone))) && + days.forall(_.active(context, timezone)) + } + def empty: Boolean = { + begin.isEmpty && end.isEmpty && hourPeriods.isEmpty && days.isEmpty + } +} + +sealed trait LegacyCompatibleCondition { + def active(requestContext: RequestContext, featureId: String): Boolean +} +case class DateRangeActivationCondition(begin: Option[Instant] = None, end: Option[Instant] = None, timezone: ZoneId) extends LegacyCompatibleCondition { + def active(context: RequestContext, featureId: String): Boolean = { + val now = context.now + begin.forall(i => i.atZone(timezone).toInstant.isBefore(now)) && end.forall(i => i.atZone(timezone).toInstant.isAfter(now)) + } +} + + +case class ZonedHourPeriod(hourPeriod: HourPeriod, timezone: ZoneId) extends LegacyCompatibleCondition { + def active(context: RequestContext, featureId: String): Boolean = { + val zonedStart = LocalDateTime.of(LocalDate.now(), hourPeriod.startTime) + .atZone(timezone).toInstant + + val zonedEnd = LocalDateTime.of(LocalDate.now(), hourPeriod.endTime) + .atZone(timezone).toInstant + + zonedStart.isBefore(context.now) && zonedEnd.isAfter(context.now) + } +} + +case class HourPeriod(startTime: LocalTime, endTime: LocalTime) { + def active(context: RequestContext, timezone: ZoneId): Boolean = { + val zonedStart = LocalDateTime.of(LocalDate.now(), startTime) + .atZone(timezone).toInstant + + val zonedEnd = LocalDateTime.of(LocalDate.now(), endTime) + .atZone(timezone).toInstant + + zonedStart.isBefore(context.now) && zonedEnd.isAfter(context.now) + } +} + +case class ActivationDayOfWeeks(days: Set[DayOfWeek]) { + def active(context: RequestContext, timezone: ZoneId): Boolean = days.contains(context.now.atZone(timezone).getDayOfWeek) +} + +case class RequestContext(tenant: String, user: String, context: FeatureContextPath = FeatureContextPath(), now: Instant = Instant.now(), data: JsObject = Json.obj()) { + def wasmJson: JsValue = Json.obj("tenant" -> tenant, "id" -> user, "now" -> now.toEpochMilli, "data" -> data) +} +sealed trait ActivationRule extends LegacyCompatibleCondition { + override def active(context: RequestContext, featureId: String): Boolean +} +object All extends ActivationRule { + override def active(context: RequestContext, featureId: String): Boolean = true +} +case class UserList(users: Set[String]) extends ActivationRule { + override def active(context: RequestContext, featureId: String): Boolean = users.contains(context.user) +} +case class UserPercentage(percentage: Int) extends ActivationRule { + override def active(context: RequestContext, featureId: String): Boolean = + Feature.isPercentageFeatureActive(s"${featureId}-${context.user}", percentage) +} + +case class ActivationCondition(period: FeaturePeriod = FeaturePeriod(), rule: ActivationRule = All) { + def active(requestContext: RequestContext, featureId: String): Boolean = + period.active(requestContext) && rule.active(requestContext, featureId) +} + +sealed trait AbstractFeature { + val id: String + val name: String + val description: String + val project: String + val enabled: Boolean + val tags: Set[String] = Set() + val metadata: JsObject = JsObject.empty + def active(requestContext: RequestContext, env: Env): Future[Either[IzanamiError, Boolean]] + + def withProject(project: String): AbstractFeature + def withId(id: String): AbstractFeature + def withName(name: String): AbstractFeature +} + +case class SingleConditionFeature( + override val id: String, + override val name: String, + override val project: String, + condition: LegacyCompatibleCondition, + override val enabled: Boolean, + override val tags: Set[String] = Set(), + override val metadata: JsObject = JsObject.empty, + override val description: String +) extends AbstractFeature { + + def toModernFeature: Feature = { + val activationCondition = this.condition match { + case DateRangeActivationCondition(begin, end, timezone) => ActivationCondition(period=FeaturePeriod(begin=begin, end=end, timezone=timezone)) + case ZonedHourPeriod(HourPeriod(startTime, endTime), timezone) => ActivationCondition(period=FeaturePeriod(hourPeriods=Set(HourPeriod(startTime = startTime, endTime = endTime)), timezone=timezone)) + case rule: ActivationRule => ActivationCondition(rule=rule) + } + + Feature(id = id, name = name, project = project, conditions = Set(activationCondition), enabled = enabled, tags = tags, metadata = metadata, description = description) + } + override def active(requestContext: RequestContext, env: Env): Future[Either[IzanamiError, Boolean]] = { + if(enabled) Future.successful(Right(condition.active(requestContext, id))) else Future.successful(Right(false)) + } + + override def withProject(project: String): SingleConditionFeature = copy(project = project) + + override def withId(id: String): SingleConditionFeature = copy(id = id) + + override def withName(name: String): SingleConditionFeature = copy(name = name) +} + +case class Feature( + override val id: String, + override val name: String, + override val project: String, + conditions: Set[ActivationCondition], + override val enabled: Boolean, + override val tags: Set[String] = Set(), + override val metadata: JsObject = JsObject.empty, + override val description: String +) extends AbstractFeature { + override def active(requestContext: RequestContext, env: Env): Future[Either[IzanamiError, Boolean]] = { + implicit val ec: ExecutionContext = env.executionContext + Future(Right { enabled && (conditions.isEmpty || conditions.exists(cond => cond.active(requestContext, name))) }) + } + + override def withProject(project: String): Feature = copy(project = project) + override def withId(id: String): Feature = copy(id = id) + override def withName(name: String): Feature = copy(name = name) +} + +case class WasmFeature( + override val id: String, + override val name: String, + override val project: String, + override val enabled: Boolean, + wasmConfig: WasmConfig, + override val tags: Set[String] = Set(), + override val metadata: JsObject = JsObject.empty, + override val description: String +) extends AbstractFeature { + override def active(requestContext: RequestContext, env: Env): Future[Either[IzanamiError, Boolean]] = { + implicit val ec: ExecutionContext = env.executionContext + if (!enabled) { + Future { Right(false) } + } else { + WasmUtils.handle(wasmConfig, requestContext)(ec, env) + } + } + override def withProject(project: String): WasmFeature = copy(project = project) + override def withId(id: String): WasmFeature = copy(id = id) + override def withName(name: String): WasmFeature = copy(name = name) +} + +object WasmFeature { + def fromJsons(value: JsValue): WasmFeature = + try { + format.reads(value).get + } catch { + case e: Throwable => throw e + } + val format: Format[WasmFeature] = new Format[WasmFeature] { + override def writes(o: WasmFeature): JsValue = Json.obj( + "id" -> o.id, + "name" -> o.name, + "enabled" -> o.enabled, + "project" -> o.project, + "config" -> o.wasmConfig.json, + "metadata" -> o.metadata, + "description" -> o.description, + "tags" -> JsArray(o.tags.map(JsString.apply).toSeq) + ) + override def reads(json: JsValue): JsResult[WasmFeature] = Try { + WasmFeature( + id = (json \ "id").as[String], + name = (json \ "name").as[String], + project = (json \ "project").as[String], + enabled = (json \ "enabled").as[Boolean], + wasmConfig = (json \ "config").as(WasmConfig.format), + metadata = (json \ "metadata").asOpt[JsObject].getOrElse(Json.obj()), + tags = (json \ "tags").asOpt[Set[String]].getOrElse(Set.empty[String]), + description = (json \ "description").asOpt[String].getOrElse("") + ) + } match { + case Failure(ex) => JsError(ex.getMessage) + case Success(value) => JsSuccess(value) + } + } +} + +case class FeatureTagRequest( + oneTagIn: Set[String] = Set(), + allTagsIn: Set[String] = Set() +) { + def isEmpty: Boolean = oneTagIn.isEmpty && allTagsIn.isEmpty + def tags: Set[String] = oneTagIn ++ allTagsIn +} + +object FeatureTagRequest { + def processInputSeqString(input: Seq[String]): Set[String] = { + input.filter(str => str.nonEmpty).flatMap(str => str.split(",")).toSet + } + + implicit def queryStringBindable(implicit + seqBinder: QueryStringBindable[Seq[String]] + ): QueryStringBindable[FeatureTagRequest] = + new QueryStringBindable[FeatureTagRequest] { + override def bind(key: String, params: Map[String, Seq[String]]): Option[Either[String, FeatureTagRequest]] = { + for { + eitherAllTagsIn <- seqBinder.bind("allTagsIn", params) + eitherOneTagIn <- seqBinder.bind("oneTagIn", params) + } yield { + Right( + FeatureTagRequest( + allTagsIn = processInputSeqString(eitherAllTagsIn.getOrElse(Seq())), + oneTagIn = processInputSeqString(eitherOneTagIn.getOrElse(Seq())) + ) + ) + } + } + override def unbind(key: String, request: FeatureTagRequest): String = { + val params = request.allTagsIn + .map(t => s"allTagsIn=${t}") + .concat(request.oneTagIn.map(t => s"oneTagIn=${t}")) + if (params.isEmpty) + "" + else + "?" + params.mkString("&") + } + } +} + +case class FeatureRequest( + projects: Set[UUID] = Set(), + features: Set[String] = Set(), + oneTagIn: Set[UUID] = Set(), + allTagsIn: Set[UUID] = Set(), + noTagIn: Set[UUID] = Set(), + context: Seq[String] = Seq() +) { + def isEmpty: Boolean = + projects.isEmpty && oneTagIn.isEmpty && allTagsIn.isEmpty && noTagIn.isEmpty && features.isEmpty +} + +object FeatureRequest { + + def processInputSeqUUID(input: Seq[String]): Set[UUID] = { + input.filter(str => str.nonEmpty).flatMap(str => str.split(",")).map(UUID.fromString).toSet + } + + def processInputSeqString(input: Seq[String]): Set[String] = { + input.filter(str => str.nonEmpty).flatMap(str => str.split(",")).toSet + } + + implicit def queryStringBindable(implicit + seqBinder: QueryStringBindable[Seq[String]] + ): QueryStringBindable[FeatureRequest] = + new QueryStringBindable[FeatureRequest] { + override def bind(key: String, params: Map[String, Seq[String]]): Option[Either[String, FeatureRequest]] = { + for { + eitherProjects <- seqBinder.bind("projects", params) + eitherFeatures <- seqBinder.bind("features", params) + eitherAllTagsIn <- seqBinder.bind("allTagsIn", params) + eitherOneTagIn <- seqBinder.bind("oneTagIn", params) + eitherNoTagIn <- seqBinder.bind("noTagIn", params) + eitherContext <- seqBinder.bind("context", params) + } yield { + Right( + FeatureRequest( + features = processInputSeqString(eitherFeatures.getOrElse(Seq())), + projects = processInputSeqUUID(eitherProjects.getOrElse(Seq())), + allTagsIn = processInputSeqUUID(eitherAllTagsIn.getOrElse(Seq())), + oneTagIn = processInputSeqUUID(eitherOneTagIn.getOrElse(Seq())), + noTagIn = processInputSeqUUID(eitherNoTagIn.getOrElse(Seq())), + context = (eitherContext + .map(seq => seq.filter(str => str.nonEmpty).flatMap(str => str.split("/"))) + .getOrElse(Seq())) + ) + ) + } + } + override def unbind(key: String, request: FeatureRequest): String = { + val params = request.projects + .map(p => s"projects=${p}") + .concat(request.allTagsIn.map(t => s"allTagsIn=${t}")) + .concat(request.oneTagIn.map(t => s"oneTagIn=${t}")) + if (params.isEmpty) + "" + else + "?" + params.mkString("&") + } + } +} + +object Feature { + + def isPercentageFeatureActive(source: String, percentage: Int): Boolean = { + val hash = (Math.abs(MurmurHash3.bytesHash(source.getBytes, 42)) % 100) + 1 + hash <= percentage + } + + def writeFeatureForCheck(feature: AbstractFeature, context: RequestContext, env: Env): Future[Either[IzanamiError, JsObject]] = { + feature + .active(context, env) + .map(either => { + either.map(active => { + Json.obj( + "name" -> feature.name, + "active" -> active, + "project" -> feature.project + ) + /*(feature match { + case w: WasmFeature => Feature.featureWrite.writes(w).as[JsObject] - "wasmConfig" + case lf: SingleConditionFeature => Feature.featureWrite.writes(lf.toModernFeature).as[JsObject] + case f => Feature.featureWrite.writes(f).as[JsObject] + }) - "metadata" ++ Json.obj("active" -> active)*/ + }) + })(env.executionContext) + } + + def writeFeatureForCheckInLegacyFormat(feature: AbstractFeature, context: RequestContext, env: Env): Future[Either[IzanamiError, Option[JsObject]]] = { + writeFeatureInLegacyFormat(feature) match { + case None => { + Future.successful(Right(None:Option[JsObject])) + } + case Some(jsObject) => { + feature.active(context, env).map { + case Left(error) => Left(error) + case Right(active) => Right(Some(jsObject ++ Json.obj("active" -> active))) + }(env.executionContext) + } + } + } + + def writeFeatureInLegacyFormat(feature: AbstractFeature): Option[JsObject] = { + feature match { + case s: SingleConditionFeature => Some(Json.toJson(OldFeature.fromModernFeature(s))(OldFeature.oldFeatureWrites).as[JsObject]) + // Transforming modern feature to script feature is a little hacky, however it's a format that legacy client + // can understand, moreover due to the script nature of the feature, there won't be cache client side, which + // is what we want since legacy client can't evaluate modern feeature locally + case f: Feature => Some(Json.toJson(OldGlobalScriptFeature(id = f.id, name = f.name, enabled = f.enabled, description = Option(f.description), tags = f.tags, ref = "fake-script-feature"))(OldFeature.oldGlobalScriptWrites).as[JsObject]) + case w: WasmFeature => Some(Json.toJson(OldFeature.fromScriptFeature(w))(OldFeature.oldFeatureWrites).as[JsObject]) + } + } + + implicit val offsetTimeWrites: Writes[OffsetTime] = Writes[OffsetTime] { time => + Json.toJson(time.format(DateTimeFormatter.ISO_OFFSET_TIME)) + } + + implicit val offsetTimeReads: Reads[OffsetTime] = Reads[OffsetTime] { json => + try { + JsSuccess(OffsetTime.parse(json.as[String], DateTimeFormatter.ISO_OFFSET_TIME)) + } catch { + case e: DateTimeParseException => JsError("Invalid time format") + } + } +val hourFormatter: DateTimeFormatter = DateTimeFormatter.ofPattern("HH:mm:ss") + implicit val hourPeriodWrites: Writes[HourPeriod] = Writes[HourPeriod] { p => + Json.obj( + "startTime" -> p.startTime.format(hourFormatter), + "endTime" -> p.endTime.format(hourFormatter) + ) + } + + implicit val hourPeriodReads: Reads[HourPeriod] = Reads[HourPeriod] { json => + (for ( + start <- (json \ "startTime").asOpt[LocalTime]; + end <- (json \ "endTime").asOpt[LocalTime] + ) + yield JsSuccess( + HourPeriod( + startTime = start, + endTime = end + ) + )).getOrElse(JsError("Failed to parse hour period")) + + } + + implicit val dayOfWeekWrites: Writes[DayOfWeek] = Writes[DayOfWeek] { d => + Json.toJson(d.name) + } + + implicit val dayOfWeekReads: Reads[DayOfWeek] = Reads[DayOfWeek] { json => + json.asOpt[String].map(DayOfWeek.valueOf).map(JsSuccess(_)).getOrElse(JsError(s"Incorrect day of week : ${json}")) + } + + implicit val activationDayOfWeekWrites: Writes[ActivationDayOfWeeks] = Writes[ActivationDayOfWeeks] { a => + Json.obj( + "days" -> a.days + ) + } + + implicit val activationDayOfWeekReads: Reads[ActivationDayOfWeeks] = Reads[ActivationDayOfWeeks] { json => + (for ( + days <- (json \ "days").asOpt[Set[DayOfWeek]] + ) yield JsSuccess(ActivationDayOfWeeks(days = days))) + .getOrElse(JsError("Failed to parse day of week period")) + } + + implicit val featurePeriodeWrite: Writes[FeaturePeriod] = Writes[FeaturePeriod] { period => + if (period.empty) { + JsNull + } else { + Json.obj( + "begin" -> period.begin, + "end" -> period.end, + "hourPeriods" -> period.hourPeriods, + "activationDays" -> period.days, + "timezone" -> period.timezone + ) + } + } + + implicit val activationRuleWrite: Writes[ActivationRule] = Writes[ActivationRule] { + case All => + Json.obj( + ) + case UserList(users) => + Json.obj( + "users" -> users + ) + case UserPercentage(percentage) => + Json.obj( + "percentage" -> percentage + ) + } + + implicit val activationConditionWrite: Writes[ActivationCondition] = Writes[ActivationCondition] { cond => + Json.obj( + "period" -> cond.period, + "rule" -> cond.rule + ) + } + + val featureWrite: Writes[AbstractFeature] = Writes[AbstractFeature] { + case Feature(id, name, project, conditions, enabled, tags, metadata, description) => { + Json.obj( + "name" -> name, + "enabled" -> enabled, + "metadata" -> metadata, + "tags" -> tags, + "conditions" -> conditions, + "id" -> id, + "project" -> project, + "description" -> description + ) + } + case WasmFeature(id, name, project, enabled, wasmConfig, tags, metadata, description) => { + Json.obj( + "name" -> name, + "enabled" -> enabled, + "metadata" -> metadata, + "tags" -> tags, + "wasmConfig" -> WasmConfig.format.writes(wasmConfig), + "id" -> id, + "project" -> project, + "description" -> description + ) + } + case SingleConditionFeature(id, name, project, condition, enabled, tags, metadata, description) => { + Json.obj( + "name" -> name, + "enabled" -> enabled, + "metadata" -> metadata, + "tags" -> tags, + "conditions" -> condition, + "id" -> id, + "project" -> project, + "description" -> description + ) + } + } + + implicit val legacyCompatibleConditionWrites: Writes[LegacyCompatibleCondition] = { + case DateRangeActivationCondition(begin, end, timezone) => { + Json.obj( + "timezone" -> timezone + ) + .applyOnWithOpt(begin) { (json, begin) => json ++ Json.obj("begin" -> begin) } + .applyOnWithOpt(end) { (json, end) => json ++ Json.obj("end" -> end) } + } + case ZonedHourPeriod(hourPeriod, timezone) => hourPeriodWrites.writes(hourPeriod).as[JsObject] ++ Json.obj("timezone" -> timezone) + case All => Json.obj() + case u: UserList => activationRuleWrite.writes(u) + case u: UserPercentage => activationRuleWrite.writes(u) + } + + val NAME_REGEXP_PATTERN: Regex = "^[a-zA-Z0-9:_-]+$".r + + implicit val activationPeriodRead: Reads[FeaturePeriod] = json => { + val maybeHourPeriod = (json \ "hourPeriods").asOpt[Set[HourPeriod]].getOrElse(Set()) + val maybeActivationDays = (json \ "activationDays").asOpt[ActivationDayOfWeeks] + val maybeBegin = (json \ "begin").asOpt[Instant](instantReads(DateTimeFormatter.ISO_OFFSET_DATE_TIME)) + val maybeEnd = (json \ "end").asOpt[Instant](instantReads(DateTimeFormatter.ISO_OFFSET_DATE_TIME)) + val maybeZone = (json \ "timezone").asOpt[String].map(str => ZoneId.of(str)) + + JsSuccess( + FeaturePeriod( + begin = maybeBegin, + end = maybeEnd, + hourPeriods = maybeHourPeriod, + days = maybeActivationDays, + timezone = maybeZone.getOrElse(ZoneId.systemDefault()) // TODO should this be allowed ? + ) + ) + } + + implicit val activationRuleRead: Reads[ActivationRule] = json => { + if (json.equals(Json.obj())) { + JsSuccess(All) + } else { + (for (percentage <- (json \ "percentage").asOpt[Int]) yield UserPercentage(percentage = percentage)) + .orElse( + for (users <- (json \ "users").asOpt[Seq[String]]) yield UserList(users = users.toSet) + ) + .map(JsSuccess(_)) + .getOrElse(JsError("Invalid activation rule")) + } + } + + implicit val activationConditionRead: Reads[ActivationCondition] = json => { + val maybeRule = (json \ "rule").asOpt[ActivationRule]; + val maybePeriod = (json \ "period").asOpt[FeaturePeriod]; + + if (maybeRule.isDefined || maybePeriod.isDefined) { + JsSuccess(ActivationCondition(rule = maybeRule.getOrElse(All), period = maybePeriod.getOrElse(FeaturePeriod()))) + } else { + JsError("Invalid activation condition") + } + } + + implicit val legacyActivationConditionRead: Reads[LegacyCompatibleCondition] = json => { + (json \ "percentage").asOpt[Int].map(p => UserPercentage(p)) + .orElse({(json \ "users").asOpt[Seq[String]].map(s => UserList(s.toSet))}) + .orElse({ + val from = (json \ "begin").asOpt[Instant](instantReads(DateTimeFormatter.ISO_OFFSET_DATE_TIME)) + val to = (json \ "end").asOpt[Instant](instantReads(DateTimeFormatter.ISO_OFFSET_DATE_TIME)) + + (json \ "timezone").asOpt[ZoneId].flatMap(zone => { + (from, to) match { + case (f@Some(_), t@Some(_)) => Some(DateRangeActivationCondition(begin = f, end = t, timezone = zone)) + case (f@Some(_), None) => Some(DateRangeActivationCondition(begin = f, end = None, timezone = zone)) + case (None, t@Some(_)) => Some(DateRangeActivationCondition(begin = None, end = t, timezone = zone)) + case _ => None + } + }) + }) + .orElse({ + for( + from <- (json \ "startTime").asOpt[LocalTime]; + to <- (json \ "endTime").asOpt[LocalTime]; + timezone <- (json \ "timezone").asOpt[ZoneId] + ) yield ZonedHourPeriod(HourPeriod(startTime = from, endTime = to), timezone) + }).map(cond => JsSuccess(cond)).getOrElse(if(json.asOpt[JsObject].exists(obj => obj.value.isEmpty)) JsSuccess(All) else JsError("Failed to read condition")) + } + + // This read is used both for parsing inputs and DB results, it may be wise to split it ... + def readFeature(json: JsValue, project: String = null): JsResult[AbstractFeature] = { + val metadata = json.select("metadata").asOpt[JsObject].getOrElse(JsObject.empty) + val id = json.select("id").asOpt[String].orNull + val description = json.select("description").asOpt[String].getOrElse("") + val tags = (json \ "tags") + .asOpt[Set[String]] + .getOrElse(Set()) + val maybeArray = (json \ "conditions").toOption + .flatMap(conds => conds.asOpt[JsArray]) + + val maybeWasmConfig = (json \ "wasmConfig").asOpt[WasmConfig](WasmConfig.format) + + val jsonProject = json + .select("project") + .asOpt[String] + .getOrElse(project) + + val parsedConditions = if ((maybeArray.isEmpty && (json \ "activationStrategy").isEmpty) || maybeArray.exists(v => v.value.isEmpty)) { + JsSuccess(Set[ActivationCondition]()) + } else if(maybeArray.isEmpty) { + JsError("Incorrect condition format") + } else { + val result = maybeArray.get.value.map(v => activationConditionRead.reads(v)).toSet + if (result.exists(r => r.isError)) { + JsError("Incorrect condition format") + } else { + JsSuccess(result.map(r => r.get)) + } + } + + val maybeLegacyCompatibleCondition: Option[LegacyCompatibleCondition] = (json \ "conditions") + .asOpt[LegacyCompatibleCondition] + + val maybeFeature: Option[JsResult[AbstractFeature]] = + for ( + enabled <- json.select("enabled").asOpt[Boolean]; + name <- json.select("name").asOpt[String].filter(name => NAME_REGEXP_PATTERN.pattern.matcher(name).matches()) + ) + yield { + (parsedConditions, maybeWasmConfig, maybeLegacyCompatibleCondition) match { + case (_, _, Some(legacyCondition)) => JsSuccess( + SingleConditionFeature( + id = id, + name = name, + enabled = enabled, + condition = legacyCondition, + tags = tags, + metadata = metadata, + project = jsonProject, + description = description + ) + ) + case (_, Some(wasmConfig), _) => { + JsSuccess( + WasmFeature( + id = id, + name = name, + project = jsonProject, + enabled = enabled, + wasmConfig = wasmConfig, + tags = tags, + metadata = metadata, + description = description + ) + ) + } + case (JsSuccess(conditions, _), None, _) => { + val jsonProject = json.select("project").asOpt[String].getOrElse(project) + + JsSuccess( + Feature( + id = id, + name = name, + enabled = enabled, + conditions = conditions, + tags = tags, + metadata = metadata, + project = jsonProject, + description = description + ) + ) + } + case _ => { + oldFeatureReads.reads(json).flatMap(f => { + // TODO handle missing timezon + f.toFeature(project, (json \ "timezone").asOpt[ZoneId].orNull, Map()) match { + case Left(err) => JsError(err) + case Right((feature, _)) => JsSuccess(feature) + } + }) + } + } + } + maybeFeature + .getOrElse(JsError("Incorrect feature format")) + + } +} + +object CustomBinders { + implicit def instantQueryStringBindable(implicit + seqBinder: QueryStringBindable[String] + ): QueryStringBindable[Instant] = + new QueryStringBindable[Instant] { + override def bind(key: String, params: Map[String, Seq[String]]): Option[Either[String, Instant]] = { + seqBinder + .bind("date", params) + .map(e => e.map(v => Instant.from(DateTimeFormatter.ISO_OFFSET_DATE_TIME.parse(v)))) + } + override def unbind(key: String, request: Instant): String = { + DateTimeFormatter.ISO_OFFSET_TIME.format(request) + } + } +} diff --git a/app/fr/maif/izanami/models/IzanamiConfiguration.scala b/app/fr/maif/izanami/models/IzanamiConfiguration.scala new file mode 100644 index 000000000..eebb5c040 --- /dev/null +++ b/app/fr/maif/izanami/models/IzanamiConfiguration.scala @@ -0,0 +1,166 @@ +package fr.maif.izanami.models + +import fr.maif.izanami.mail.MailGunRegions.MailGunRegion +import fr.maif.izanami.mail.MailerTypes.{MailJet, MailerType, SMTP} +import fr.maif.izanami.mail._ +import fr.maif.izanami.models.InvitationMode.InvitationMode +import fr.maif.izanami.utils.syntax.implicits.BetterSyntax +import io.otoroshi.wasm4s.scaladsl.WasmoSettings +import play.api.libs.functional.syntax.toFunctionalBuilderOps +import play.api.libs.json.Reads.instantReads +import play.api.libs.json._ + +import java.time.Instant +import java.time.format.DateTimeFormatter + +case class OIDCConfiguration( + clientId: String, + clientSecret: String, + authorizeUrl: String, + tokenUrl: String, + redirectUrl: String +) + +case class IzanamiConfiguration( + mailer: MailerType, + invitationMode: InvitationMode, + originEmail: Option[String], + anonymousReporting: Boolean, + anonymousReportingLastAsked: Option[Instant] +) + +case class FullIzanamiConfiguration( + invitationMode: InvitationMode, + originEmail: Option[String], + mailConfiguration: MailProviderConfiguration, + anonymousReporting: Boolean, + anonymousReportingLastAsked: Option[Instant] +) + +object InvitationMode extends Enumeration { + type InvitationMode = Value + val Mail, Response = Value +} + +object IzanamiConfiguration { + implicit val mailerReads: Reads[MailerType] = { json => + json + .asOpt[String] + .flatMap(str => MailerTypes.values.find(v => str.equalsIgnoreCase(v.toString))) + .map(JsSuccess(_)) + .getOrElse(JsError(s"${json} is not a correct right level")) + } + + implicit val mailGunRegionReads: Reads[MailGunRegion] = { json => + json + .asOpt[String] + .flatMap(str => MailGunRegions.values.find(v => str.equalsIgnoreCase(v.toString))) + .map(JsSuccess(_)) + .getOrElse(JsError(s"${json} is not a correct right level")) + } + + implicit val invitationModeReads: Reads[InvitationMode] = { json => + json + .asOpt[String] + .flatMap(str => InvitationMode.values.find(v => str.equalsIgnoreCase(v.toString))) + .map(JsSuccess(_)) + .getOrElse(JsError(s"${json} is not a correct right level")) + } + + implicit val mailJetConfigurationReads: Reads[MailJetConfiguration] = ( + (__ \ "apiKey").read[String] and + (__ \ "secret").read[String] and + (__ \ "url").readNullable[String] + )((apiKey, secret, url) => MailJetConfiguration(apiKey = apiKey, secret = secret, url = url)) + + implicit val mailGunConfigurationReads: Reads[MailGunConfiguration] = ( + (__ \ "apiKey").read[String] and + (__ \ "url").readNullable[String] and + (__ \ "region").read[MailGunRegion] + )((apiKey, url, region) => MailGunConfiguration(apiKey = apiKey, url = url, region = region)) + + implicit val SMTPConfigurationReads: Reads[SMTPConfiguration] = ( + (__ \ "host").read[String] and + (__ \ "port").readNullable[Int] and + (__ \ "user").readNullable[String] and + (__ \ "password").readNullable[String] and + (__ \ "auth").read[Boolean] and + (__ \ "starttlsEnabled").read[Boolean] and + (__ \ "smtps").read[Boolean] + )((host, maybePort, maybeUser, maybePassword, auth, starttls, smtps) => + SMTPConfiguration(host = host, port = maybePort, user = maybeUser, password = maybePassword ,auth=auth, starttlsEnabled = starttls, smtps=smtps) + ) + + implicit val SMTPConfigurationWrites: Writes[SMTPConfiguration] = conf => { + Json.obj( + "host" -> conf.host, + "auth" -> conf.auth, + "starttlsEnabled" -> conf.starttlsEnabled, + "smtps" -> conf.smtps + ) + .applyOnWithOpt(conf.port) { (json, port) => json ++ Json.obj("port" -> port) } + .applyOnWithOpt(conf.user) { (json, user) => json ++ Json.obj("user" -> user) } + .applyOnWithOpt(conf.password) { (json, password) => json ++ Json.obj("password" -> password) } + } + + implicit val mailProviderConfigurationReads: (MailerType => Reads[MailProviderConfiguration]) = mailerType => + json => { + (mailerType match { + case MailerTypes.MailGun => { + json.asOpt[MailGunConfiguration].map(conf => MailGunMailProvider(conf)) + } + case MailJet => { + json.asOpt[MailJetConfiguration].map(conf => MailJetMailProvider(conf)) + } + case SMTP => { + json.asOpt[SMTPConfiguration].map(conf => SMTPMailProvider(conf)) + } + case MailerTypes.Console => Some(ConsoleMailProvider) + }).map(JsSuccess(_)) + .getOrElse(JsError("Bad mail configuration format")) + } + + implicit val mailJetConfigurationWrites: Writes[MailJetConfiguration] = json => { + Json.obj( + "url" -> json.url, + "apiKey" -> json.apiKey, + "secret" -> json.secret + ) + } + + implicit val mailGunConfigurationWrite: Writes[MailGunConfiguration] = json => { + Json.obj( + "url" -> json.url, + "apiKey" -> json.apiKey, + "region" -> json.region.toString.toUpperCase + ) + } + + implicit val configurationReads: Reads[IzanamiConfiguration] = json => { + (for ( + mailer <- (json \ "mailer").asOpt[MailerType]; + invitationMode <- (json \ "invitationMode").asOpt[InvitationMode]; + anonymousReporting <- (json \ "anonymousReporting").asOpt[Boolean] + ) yield { + val anonymousReportingLastAsked = (json \ "anonymousReportingLastAsked").asOpt[Instant](instantReads(DateTimeFormatter.ISO_OFFSET_DATE_TIME)) + val originEmail = (json \ "originEmail").asOpt[String] + (mailer, originEmail) match { + case (MailerTypes.Console, _) => + JsSuccess(IzanamiConfiguration(mailer = mailer, invitationMode = invitationMode, originEmail = originEmail, anonymousReporting=anonymousReporting, anonymousReportingLastAsked=anonymousReportingLastAsked)) + case (_, None) => JsError("Origin email is missing") + case (_, maybeEmail) => + JsSuccess(IzanamiConfiguration(mailer = mailer, invitationMode = invitationMode, originEmail = maybeEmail, anonymousReporting=anonymousReporting, anonymousReportingLastAsked=anonymousReportingLastAsked)) + } + }).getOrElse(JsError("Bad body format")) + } + + implicit val configurationWrites: Writes[IzanamiConfiguration] = conf => { + Json.obj( + "mailer" -> conf.mailer.toString, + "invitationMode" -> conf.invitationMode.toString, + "originEmail" -> conf.originEmail, + "anonymousReporting" -> conf.anonymousReporting, + "anonymousReportingLastAsked" -> conf.anonymousReportingLastAsked + ) + } +} diff --git a/app/fr/maif/izanami/models/Projects.scala b/app/fr/maif/izanami/models/Projects.scala new file mode 100644 index 000000000..e3f798cdf --- /dev/null +++ b/app/fr/maif/izanami/models/Projects.scala @@ -0,0 +1,46 @@ +package fr.maif.izanami.models + +import play.api.libs.json.Format.GenericFormat +import play.api.libs.json.OFormat.oFormatFromReadsAndOWrites +import play.api.libs.json._ + +import java.util.UUID +import scala.util.matching.Regex + +trait ProjectQueryResult + +case class Project(id: UUID, name: String, features: List[AbstractFeature] = List(), description: String) extends ProjectQueryResult + +case class EmptyProjectRow() extends ProjectQueryResult + +case class ProjectCreationRequest(name: String, description: String) + +object Project { + val projectReads: Reads[ProjectCreationRequest] = { json => + (json \ "name") + .asOpt[String] + .filter(name => PROJECT_REGEXP.pattern.matcher(name).matches()) + .map(name => JsSuccess(ProjectCreationRequest(name = name, description=(json \ "description").asOpt[String].getOrElse("")))) + .getOrElse(JsError("Invalid project")) + } + private val PROJECT_REGEXP: Regex = "^[a-zA-Z0-9_-]+$".r + + implicit val dbProjectReads: Reads[Project] = { json => + { + for ( + name <- (json \ "name").asOpt[String]; + id <- (json \ "id").asOpt[UUID]; + description <- (json \ "description").asOpt[String] + ) yield JsSuccess(Project(name = name, description=description, id=id)) + }.getOrElse(JsError("Error reading project")) + } + + implicit val projectWrites: Writes[Project] = { project => + Json.obj( + "name" -> project.name, + "id" -> project.id, + "features" -> {project.features.map(f => Feature.featureWrite.writes(f))}, + "description" -> project.description + ) + } +} diff --git a/app/fr/maif/izanami/models/Tags.scala b/app/fr/maif/izanami/models/Tags.scala new file mode 100644 index 000000000..948518786 --- /dev/null +++ b/app/fr/maif/izanami/models/Tags.scala @@ -0,0 +1,35 @@ +package fr.maif.izanami.models + +import play.api.libs.json._ + +import java.util.UUID +import scala.util.matching.Regex + +case class Tag(id: UUID, name: String, description: String) +case class TagCreationRequest(name: String, description: String = "") + +object Tag { + val tagRequestReads: Reads[TagCreationRequest] = { json => + val maybeRequest = + for (name <- (json \ "name").asOpt[String].filter(id => TAG_REGEXP.pattern.matcher(id).matches())) + yield TagCreationRequest(name, description = (json \ "description").asOpt[String].getOrElse("")) + + maybeRequest.map(JsSuccess(_)).getOrElse(JsError("Error reading tag creation request")) + } + val tagReads: Reads[Tag] = { json => + { + for ( + name <- (json \ "name").asOpt[String].filter(id => TAG_REGEXP.pattern.matcher(id).matches()) + ) yield JsSuccess(Tag(name = name, description = (json \ "description").asOpt[String].orNull, id = (json \ "id").asOpt[UUID].orNull)) + }.getOrElse(JsError("Error reading tag")) + } + private val TAG_REGEXP: Regex = "^[a-zA-Z0-9_-]+$".r + + implicit val tagWrites: Writes[Tag] = { tag => + Json.obj( + "id" -> tag.id, + "name" -> tag.name, + "description" -> tag.description + ) + } +} diff --git a/app/fr/maif/izanami/models/Tenants.scala b/app/fr/maif/izanami/models/Tenants.scala new file mode 100644 index 000000000..27584ebe5 --- /dev/null +++ b/app/fr/maif/izanami/models/Tenants.scala @@ -0,0 +1,34 @@ +package fr.maif.izanami.models + +import fr.maif.izanami.models.Project.dbProjectReads +import play.api.libs.json.Format.GenericFormat +import play.api.libs.json._ + +import java.util.UUID +import scala.util.matching.Regex + +case class Tenant(name: String, projects: List[Project] = List(), tags: List[Tag] = List(), description: String = "") { + def addProject(project: Project): Tenant = Tenant(name, projects :+ project) +} +case class TenantCreationRequest(name: String, description: String = "") + +object Tenant { + val tenantReads: Reads[TenantCreationRequest] = { json => + (json \ "name") + .asOpt[String] + .filter(id => TENANT_REGEXP.pattern.matcher(id).matches()) + .map(name => TenantCreationRequest(name, description=(json \ "description").asOpt[String].getOrElse(""))) + .map(JsSuccess(_)) + .getOrElse(JsError("Invalid tenant")) + } + private val TENANT_REGEXP: Regex = "^[a-z0-9_-]+$".r + + implicit val tenantWrite: Writes[Tenant] = { tenant => + Json.obj( + "name" -> tenant.name, + "projects" -> tenant.projects, + "description" -> tenant.description, + "tags" -> tenant.tags + ) + } +} diff --git a/app/fr/maif/izanami/models/User.scala b/app/fr/maif/izanami/models/User.scala new file mode 100644 index 000000000..40a029c66 --- /dev/null +++ b/app/fr/maif/izanami/models/User.scala @@ -0,0 +1,477 @@ +package fr.maif.izanami.models + +import fr.maif.izanami.models.RightLevels.RightLevel +import fr.maif.izanami.models.RightTypes.RightType +import fr.maif.izanami.utils.syntax.implicits.BetterSyntax +import play.api.data.validation.{Constraints, Valid} +import play.api.libs.functional.syntax.toFunctionalBuilderOps +import play.api.libs.json._ +import play.api.mvc.QueryStringBindable + +import scala.collection.mutable.ArrayBuffer +import scala.util.matching.Regex + +object RightTypes extends Enumeration { + type RightType = Value + val Project, Key = Value +} + +case class RightUnit(name: String, rightType: RightType, rightLevel: RightLevel) + +object RightLevels extends Enumeration { + type RightLevel = Value + val Read, Write, Admin = Value + + def superiorOrEqualLevels(level: RightLevel): Set[Value] = + level match { + case RightLevels.Read => Set(RightLevels.Read, RightLevels.Write, RightLevels.Admin) + case RightLevels.Write => Set(RightLevels.Write, RightLevels.Admin) + case RightLevels.Admin => Set(RightLevels.Admin) + } + + implicit def queryStringBindable(implicit + stringBinder: QueryStringBindable[String] + ): QueryStringBindable[RightLevel] = + new QueryStringBindable[RightLevel] { + override def bind(key: String, params: Map[String, Seq[String]]): Option[Either[String, RightLevel]] = { + params.get(key).collect { case Seq(s) => + RightLevels.values.find(_.toString.equalsIgnoreCase(s)).toRight("Invalid right specified") + } + } + override def unbind(key: String, value: RightLevel): String = { + implicitly[QueryStringBindable[String]].unbind(key, value.toString) + } + } +} + +case class UserInvitation( + email: String, + rights: Rights = Rights.EMPTY, + admin: Boolean = false, + id: String = null +) + +case class UserRightsUpdateRequest( + rights: Rights = Rights.EMPTY, + admin: Option[Boolean] = None +) + +case class UserInformationUpdateRequest( + name: String, + email: String, + password: String, + defaultTenant: Option[String] +) + +case class UserPasswordUpdateRequest( + password: String, + oldPassword: String +) + +sealed trait UserType +case object INTERNAL extends UserType +case object OTOROSHI extends UserType +case object OIDC extends UserType + +sealed trait UserTrait { + val username: String + val email: String + val password: String = null + val admin: Boolean = false + val userType: UserType = INTERNAL + val defaultTenant: Option[String] = None + val legacy: Boolean = false + def withRights(rights: Rights): UserWithRights = + UserWithRights(username, email, password, admin, userType, rights, defaultTenant) +} + +case class User( + override val username: String, + override val email: String = null, + override val password: String = null, + override val admin: Boolean = false, + override val userType: UserType = INTERNAL, + override val defaultTenant: Option[String] = None, + override val legacy: Boolean = false +) extends UserTrait { + def withSingleLevelRight(level: RightLevel): UserWithSingleLevelRight = + UserWithSingleLevelRight(username, email, password, admin, userType, level, defaultTenant) + def withSingleProjectRightLevel(level: RightLevel, tenantAdmin: Boolean = false): UserWithSingleProjectRight = + UserWithSingleProjectRight(username, email, password, admin, userType, level, defaultTenant, tenantAdmin) +} + +case class UserWithSingleProjectRight( + override val username: String, + override val email: String = null, + override val password: String = null, + override val admin: Boolean = false, + override val userType: UserType, + right: RightLevel, + override val defaultTenant: Option[String] = None, + tenantAdmin: Boolean = false +) extends UserTrait + +case class UserWithSingleLevelRight( + override val username: String, + override val email: String = null, + override val password: String = null, + override val admin: Boolean = false, + override val userType: UserType, + right: RightLevel, + override val defaultTenant: Option[String] = None +) extends UserTrait + +case class UserWithRights( + override val username: String, + override val email: String, + override val password: String = null, + override val admin: Boolean = false, + override val userType: UserType, + rights: Rights = Rights(tenants = Map()), + override val defaultTenant: Option[String] = None, + override val legacy: Boolean = false +) extends UserTrait { + def hasAdminRightForProject(project: String, tenant: String): Boolean = { + admin || rights.tenants + .get(tenant) + .exists(tenantRight => + tenantRight.level == RightLevels.Admin || tenantRight.projects + .get(project) + .exists(r => r.level == RightLevels.Admin) + ) + } + + def hasAdminRightForKey(key: String, tenant: String): Boolean = { + admin || rights.tenants + .get(tenant) + .exists(tenantRight => + tenantRight.level == RightLevels.Admin || tenantRight.keys.get(key).exists(r => r.level == RightLevels.Admin) + ) + } + + def hasAdminRightForTenant(tenant: String): Boolean = { + admin || rights.tenants.get(tenant).exists(tenantRight => tenantRight.level == RightLevels.Admin) + } +} + +case class UserWithTenantRights( + override val username: String, + override val email: String, + override val password: String = null, + override val admin: Boolean = false, + override val userType: UserType, + override val defaultTenant: Option[String] = None, + tenantRights: Map[String, RightLevel] = Map() +) extends UserTrait + +case class UserWithCompleteRightForOneTenant( + override val username: String, + override val email: String, + override val password: String = null, + override val userType: UserType, + override val admin: Boolean = false, + override val defaultTenant: Option[String] = None, + tenantRight: Option[TenantRight] +) extends UserTrait { + def hasRightForProject(project: String, level: RightLevel): Boolean = { + tenantRight + .flatMap(tr => tr.projects.get(project)) + .exists(r => RightLevels.superiorOrEqualLevels(r.level).contains(level)) + } +} + +case class AtomicRight(level: RightLevel) + +case class TenantRight( + level: RightLevel, + projects: Map[String, AtomicRight] = Map(), + keys: Map[String, AtomicRight] = Map() +) { + def toRightUnits(): Seq[RightUnit] = { + projects + .map { case (name, right) => RightUnit(name = name, rightType = RightTypes.Project, rightLevel = right.level) } + .concat(keys.map { case (name, right) => + RightUnit(name = name, rightType = RightTypes.Key, rightLevel = right.level) + }) + .toSeq + } +} + +case class Rights(tenants: Map[String, TenantRight]) { + def withTenantRight(name: String, level: RightLevel): Rights = { + val newTenants = + if (tenants.contains(name)) tenants + (name -> tenants(name).copy(level = level)) + else tenants + (name -> TenantRight(level = level)) + copy(tenants = newTenants) + } + + def withProjectRight(name: String, tenant: String, level: RightLevel): Rights = { + val newTenants = + if (tenants.contains(tenant)) tenants else tenants + (tenant -> TenantRight(level = RightLevels.Read)) + val newProjects = newTenants(tenant).projects + (name -> AtomicRight(level = level)) + copy(tenants = newTenants + (tenant -> newTenants(tenant).copy(projects = newProjects))) + } + + def withKeyRight(name: String, tenant: String, level: RightLevel): Rights = { + val newTenants = + if (tenants.contains(tenant)) tenants else tenants + (tenant -> TenantRight(level = RightLevels.Read)) + val newKeys = newTenants(tenant).keys + (name -> AtomicRight(level = level)) + copy(tenants = newTenants + (tenant -> newTenants(tenant).copy(keys = newKeys))) + } +} + +object Rights { + case class RightDiff( + addedTenantRights: Seq[FlattenTenantRight] = Seq(), + removedTenantRights: Seq[FlattenTenantRight] = Seq(), + addedProjectRights: Map[String, Seq[FlattenProjectRight]] = Map(), + removedProjectRights: Map[String, Seq[FlattenProjectRight]] = Map(), + addedKeyRights: Map[String, Seq[FlattenKeyRight]] = Map(), + removedKeyRights: Map[String, Seq[FlattenKeyRight]] = Map() + ) + + case class TenantRightDiff( + addedTenantRight: Option[FlattenTenantRight] = Option.empty, + removedTenantRight: Option[FlattenTenantRight] = Option.empty, + addedProjectRights: Set[FlattenProjectRight] = Set(), + removedProjectRights: Set[FlattenProjectRight] = Set(), + addedKeyRights: Set[FlattenKeyRight] = Set(), + removedKeyRights: Set[FlattenKeyRight] = Set() + ) + sealed trait FlattenRight + case class FlattenTenantRight(name: String, level: RightLevel) extends FlattenRight + case class FlattenProjectRight(name: String, tenant: String, level: RightLevel) extends FlattenRight + case class FlattenKeyRight(name: String, tenant: String, level: RightLevel) extends FlattenRight + val EMPTY: Rights = Rights(tenants = Map()) + // TODO refactor me + def compare(tenantName: String, base: Option[TenantRight], modified: Option[TenantRight]): Option[TenantRightDiff] = { + def flattenProjects(tenantRight: TenantRight): Set[FlattenProjectRight] = { + tenantRight.projects.map { case (projectName, AtomicRight(level)) => + FlattenProjectRight(name = projectName, level = level, tenant = tenantName) + }.toSet + } + + def flattenKeys(tenantRight: TenantRight): Set[FlattenKeyRight] = { + tenantRight.keys.map { case (projectName, AtomicRight(level)) => + FlattenKeyRight(name = projectName, level = level, tenant = tenantName) + }.toSet + } + + (base, modified) match { + case (None, None) => None + case (Some(existingRights), None) => + Some( + TenantRightDiff( + removedTenantRight = Some(FlattenTenantRight(name = tenantName, level = existingRights.level)), + removedProjectRights = flattenProjects(existingRights), + removedKeyRights = flattenKeys(existingRights) + ) + ) + case (None, Some(newRights)) => + Some( + TenantRightDiff( + addedTenantRight = Some(FlattenTenantRight(name = tenantName, level = newRights.level)), + addedProjectRights = flattenProjects(newRights), + addedKeyRights = flattenKeys(newRights) + ) + ) + case (Some(oldR @ TenantRight(oldLevel, _, _)), Some(newR @ TenantRight(newLevel, _, _))) + if oldLevel != newLevel => { + Some( + TenantRightDiff( + addedTenantRight = Some(FlattenTenantRight(name = tenantName, level = newLevel)), + removedTenantRight = Some(FlattenTenantRight(name = tenantName, level = oldLevel)), + addedProjectRights = flattenProjects(newR).diff(flattenProjects(oldR)), + removedProjectRights = flattenProjects(oldR).diff(flattenProjects(newR)), + addedKeyRights = flattenKeys(newR).diff(flattenKeys(oldR)), + removedKeyRights = flattenKeys(oldR).diff(flattenKeys(newR)) + ) + ) + } + case (Some(oldR), Some(newR)) => { + Some( + TenantRightDiff( + addedProjectRights = flattenProjects(newR).diff(flattenProjects(oldR)), + removedProjectRights = flattenProjects(oldR).diff(flattenProjects(newR)), + addedKeyRights = flattenKeys(newR).diff(flattenKeys(oldR)), + removedKeyRights = flattenKeys(oldR).diff(flattenKeys(newR)) + ) + ) + } + } + } + + def compare(base: Rights, modified: Rights): RightDiff = { + def extractFlattenRights( + rights: Rights + ): (Set[FlattenTenantRight], Set[FlattenProjectRight], Set[FlattenKeyRight]) = { + val tenants = ArrayBuffer[FlattenTenantRight]() + val projects = ArrayBuffer[FlattenProjectRight]() + val keys = ArrayBuffer[FlattenKeyRight]() + rights.tenants.foreach { + case (tenantName, tenantRights) => { + tenants.addOne(FlattenTenantRight(name = tenantName, level = tenantRights.level)) + projects.addAll( + tenantRights.projects + .map { case (projectName, level) => + FlattenProjectRight(name = projectName, tenant = tenantName, level = level.level) + } + ) + + keys.addAll(tenantRights.keys.map { case (keyName, level) => + FlattenKeyRight(name = keyName, tenant = tenantName, level = level.level) + }) + } + } + + (tenants.toSet, projects.toSet, keys.toSet) + } + + val (baseTenantRights, baseProjectRights, baseKeyRights) = extractFlattenRights(base) + val (newTenantRights, newProjectRights, newKeyRights) = extractFlattenRights(modified) + + RightDiff( + addedTenantRights = newTenantRights.diff(baseTenantRights).toSeq, + removedTenantRights = baseTenantRights.diff(newTenantRights).toSeq, + addedProjectRights = newProjectRights.diff(baseProjectRights).toSeq.groupBy(_.tenant), + removedProjectRights = baseProjectRights.diff(newProjectRights).toSeq.groupBy(_.tenant), + addedKeyRights = newKeyRights.diff(baseKeyRights).toSeq.groupBy(_.tenant), + removedKeyRights = baseKeyRights.diff(newKeyRights).toSeq.groupBy(_.tenant) + ) + } +} + +object User { + val PASSWORD_REGEXP: Regex = "^[a-zA-Z0-9_]{8,100}$".r + + implicit val rightLevelReads: Reads[RightLevel] = { json => + json + .asOpt[String] + .flatMap(str => RightLevels.values.find(v => str.equalsIgnoreCase(v.toString))) + .map(JsSuccess(_)) + .getOrElse(JsError(s"${json} is not a correct right level")) + } + + implicit val rightReads: Reads[AtomicRight] = Json.reads[AtomicRight] + implicit val rightWrites: Writes[AtomicRight] = Json.writes[AtomicRight] + + implicit val tenantRightReads: Reads[TenantRight] = ( + (__ \ "level").read[RightLevel] and + (__ \ "projects").readWithDefault[Map[String, AtomicRight]](Map()) and + (__ \ "keys").readWithDefault[Map[String, AtomicRight]](Map()) + )(TenantRight.apply _) + + implicit val rightsReads: Reads[Rights] = + (__ \ "tenants").readWithDefault[Map[String, TenantRight]](Map()).map(Rights.apply) + + implicit val tenantRightWrite: Writes[TenantRight] = { tenantRight => + Json.obj( + "level" -> tenantRight.level.toString, + "projects" -> tenantRight.projects, + "keys" -> tenantRight.keys + ) + } + + implicit val rightWrite: Writes[Rights] = Json.writes[Rights] + + implicit val userReads: Reads[User] = ( + (__ \ "username").read[String].filter(name => NAME_REGEXP.pattern.matcher(name).matches()) and + (__ \ "email").read[String].filter(name => NAME_REGEXP.pattern.matcher(name).matches()) and + (__ \ "password").read[String].filter(name => PASSWORD_REGEXP.pattern.matcher(name).matches()) and + (__ \ "admin").readWithDefault[Boolean](false) and + (__ \ "defaultTenant").readNullable[String] + )((username, email, password, admin, defaultTenant) => + User(username = username, email = email, password = password, admin = admin, defaultTenant = defaultTenant) + ) + + implicit val userWithRightsReads: Reads[UserWithRights] = ( + userReads and + (__ \ "rights").readWithDefault[Rights](Rights(tenants = Map())) + )((user, rights) => user.withRights(rights)) + + implicit val userWrites: Writes[User] = { user => + Json.obj( + "username" -> user.username, + "email" -> user.email, + "userType" -> user.userType.toString, + "admin" -> user.admin, + "defaultTenant" -> user.defaultTenant + ) + } + + implicit val userWithTenantRightWrites: Writes[UserWithSingleLevelRight] = { user => + { + Json + .obj( + "username" -> user.username, + "email" -> user.email, + "userType" -> user.userType.toString, + "admin" -> user.admin, + "defaultTenant" -> user.defaultTenant + ) + .applyOnWithOpt(Option(user.right)) { (json, right) => json ++ Json.obj("right" -> right) } + } + } + + implicit val userWithSingleProjectRightWrites: Writes[UserWithSingleProjectRight] = { user => + { + Json + .obj( + "username" -> user.username, + "email" -> user.email, + "userType" -> user.userType.toString, + "admin" -> user.admin, + "defaultTenant" -> user.defaultTenant, + "tenantAdmin" -> user.tenantAdmin + ) + .applyOnWithOpt(Option(user.right)) { (json, right) => json ++ Json.obj("right" -> right) } + } + } + + implicit val userRightsWrites: Writes[UserWithRights] = { user => + Json.obj( + "username" -> user.username, + "admin" -> user.admin, + "rights" -> user.rights, + "email" -> user.email, + "userType" -> user.userType, + "defaultTenant" -> user.defaultTenant + ) + } + + implicit val userTypeReads: Reads[UserType] = { json => + (json.asOpt[String].map(_.toUpperCase) flatMap { + case "OTOROSHI" => OTOROSHI.some + case "INTERNAL" => INTERNAL.some + case "OIDC" => OIDC.some + case _ => None + }).map(JsSuccess(_)).getOrElse(JsError(s"Unknown user type ${json}")) + } + + implicit val userTypeWrites: Writes[UserType] = userType => JsString(userType.toString) + + implicit val userWithTenantRightsWrite: Writes[UserWithTenantRights] = Json.writes[UserWithTenantRights] + + implicit val userInvitationReads: Reads[UserInvitation] = ((__ \ "email").read[String] and + (__ \ "rights").readWithDefault[Rights](Rights.EMPTY) and + (__ \ "admin").readWithDefault[Boolean](false))((email, rights, admin) => + UserInvitation(email = email, rights = rights, admin = admin) + ) + + implicit val userRightsUpdateReads: Reads[UserRightsUpdateRequest] = ((__ \ "rights").read[Rights] and + (__ \ "admin").readNullable[Boolean])((rights, admin) => UserRightsUpdateRequest(rights = rights, admin = admin)) + + implicit val userUpdateReads: Reads[UserInformationUpdateRequest] = + ((__ \ "username").read[String].filter(name => NAME_REGEXP.pattern.matcher(name).matches()) and + (__ \ "email").read[String].filter(Constraints.emailAddress.apply(_) == Valid) and + (__ \ "defaultTenant").readNullable[String] and + (__ \ "password").read[String])((name, email, defaultTenant, password) => + UserInformationUpdateRequest(name = name, email = email, password = password, defaultTenant = defaultTenant) + ) + + implicit val userPasswordUpdateReads: Reads[UserPasswordUpdateRequest] = + ((__ \ "password").read[String].filter(name => PASSWORD_REGEXP.pattern.matcher(name).matches()) and + (__ \ "oldPassword").read[String])((password, oldPassword) => + UserPasswordUpdateRequest(oldPassword = oldPassword, password = password) + ) +} diff --git a/app/fr/maif/izanami/models/package.scala b/app/fr/maif/izanami/models/package.scala new file mode 100644 index 000000000..b5d7a53b1 --- /dev/null +++ b/app/fr/maif/izanami/models/package.scala @@ -0,0 +1,7 @@ +package fr.maif.izanami + +import scala.util.matching.Regex + +package object models { + val NAME_REGEXP: Regex = "^[a-zA-Z0-9_-]+$".r +} diff --git a/app/fr/maif/izanami/security/JwtService.scala b/app/fr/maif/izanami/security/JwtService.scala new file mode 100644 index 000000000..668584089 --- /dev/null +++ b/app/fr/maif/izanami/security/JwtService.scala @@ -0,0 +1,56 @@ +package fr.maif.izanami.security + +import fr.maif.izanami.env.Env +import fr.maif.izanami.security.JwtService.{decodeJWT, decrypt, encrypt} +import pdi.jwt.{JwtAlgorithm, JwtClaim, JwtJson} +import play.api.libs.json.JsValue + +import java.time.Instant +import java.util.Base64 +import javax.crypto.Cipher +import javax.crypto.spec.SecretKeySpec +import scala.util.Try + +class JwtService(env: Env) { + def generateToken(username: String, content: JsValue = null) = { + val secondsSinceEpoch = Instant.now().getEpochSecond + var claim = JwtClaim( + issuer = Some("Izanami"), + subject = Some(username), + expiration = Some(secondsSinceEpoch + 3600), + notBefore = Some(secondsSinceEpoch - 60), + issuedAt = Some(secondsSinceEpoch), + audience = Some(Set(env.expositionUrl)) + ) + claim = Option(content).map(c => claim.withContent(c.toString())).getOrElse(claim) + encrypt(JwtJson.encode( + claim, + env.configuration.get[String]("app.authentication.secret"), + JwtAlgorithm.HS256 + ), env.encryptionKey) + } + + def parseJWT(token: String): Try[JwtClaim] = + decodeJWT(token, env.configuration.get[String]("app.authentication.secret"), env.encryptionKey) +} + +object JwtService { + def encrypt(subject: String, secret: SecretKeySpec) = { + val cipher: Cipher = Cipher.getInstance("AES") + cipher.init(Cipher.ENCRYPT_MODE, secret) + + new String(Base64.getUrlEncoder.encode(cipher.doFinal(subject.getBytes()))) + } + + def decrypt(subject: String, secret: SecretKeySpec) = { + val cipher: Cipher = Cipher.getInstance("AES") + cipher.init(Cipher.DECRYPT_MODE, secret) + new String(cipher.doFinal(Base64.getUrlDecoder.decode(subject.getBytes()))) + } + + def decodeJWT(token: String, signingSecret: String, secret: SecretKeySpec): Try[JwtClaim] = { + // Factorize with AuthAction code + JwtJson + .decode(decrypt(token, secret), signingSecret, Seq(JwtAlgorithm.HS256)) + } +} diff --git a/app/fr/maif/izanami/security/generators.scala b/app/fr/maif/izanami/security/generators.scala new file mode 100644 index 000000000..d352ccf0a --- /dev/null +++ b/app/fr/maif/izanami/security/generators.scala @@ -0,0 +1,80 @@ +package fr.maif.izanami.security + +import java.util.UUID +import java.util.concurrent.ThreadLocalRandom +import java.util.concurrent.atomic.AtomicLong +import scala.util.Try + +class IdGenerator(generatorId: Long) { + def nextId(): Long = IdGenerator.nextId(generatorId) + def nextIdSafe(): Try[Long] = Try(nextId()) + def nextIdStr(): String = IdGenerator.nextIdStr(generatorId) +} + +object IdGenerator { + + private[this] val CHARACTERS = + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789".toCharArray.map(_.toString) + private[this] val EXTENDED_CHARACTERS = + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789*$%)([]!=+-_:/;.><&".toCharArray.map(_.toString) + private[this] val INIT_STRING = for (i <- 0 to 15) yield Integer.toHexString(i) + + private[this] val minus = 1288834974657L + private[this] val counter = new AtomicLong(-1L) + private[this] val lastTimestamp = new AtomicLong(-1L) + private[this] val duplicates = new AtomicLong(-0L) + + def apply(generatorId: Long) = new IdGenerator(generatorId) + + def nextId(generatorId: Long): Long = + synchronized { + if (generatorId > 1024L) throw new RuntimeException("Generator id can't be larger than 1024") + val timestamp = System.currentTimeMillis + if (timestamp < lastTimestamp.get()) throw new RuntimeException("Clock is running backward. Sorry :-(") + lastTimestamp.set(timestamp) + counter.compareAndSet(4095, -1L) + ((timestamp - minus) << 22L) | (generatorId << 10L) | counter.incrementAndGet() + } + + def nextIdStr(generatorId: Long): String = + synchronized { + if (generatorId > 1024L) throw new RuntimeException("Generator id can't be larger than 1024") + val timestamp = System.currentTimeMillis + val append = if (timestamp < lastTimestamp.get()) s"-${duplicates.incrementAndGet() + generatorId}" else "" + lastTimestamp.set(timestamp) + counter.compareAndSet(4095, -1L) + s"${(((timestamp - minus) << 22L) | (generatorId << 10L) | counter.incrementAndGet())}$append" + } + + def uuid: String = { + val random = ThreadLocalRandom.current() + (for { + c <- 0 to 36 + } yield c match { + case i if i == 9 || i == 14 || i == 19 || i == 24 => "-" + case i if i == 15 => "4" + case i if c == 20 => INIT_STRING((random.nextDouble() * 4.0).toInt | 8) + case i => INIT_STRING((random.nextDouble() * 15.0).toInt | 0) + }).mkString("") + } + + def token(characters: Array[String], size: Int): String = { + val random = ThreadLocalRandom.current() + (for { + i <- 0 to size - 1 + } yield characters(random.nextInt(characters.size))).mkString("") + } + + def token(size: Int): String = token(CHARACTERS, size) + def token: String = token(64) + def extendedToken(size: Int): String = token(EXTENDED_CHARACTERS, size) + def extendedToken: String = token(EXTENDED_CHARACTERS, 64) + def namedToken(prefix: String, size: Int): String = s"${prefix}_${token(size)}" + def namedId(prefix: String, env: String): String = { + env match { + case "prod" => s"${prefix}_${UUID.randomUUID().toString}" + case _ => s"${prefix}_${env}_${UUID.randomUUID().toString}" + } + } +} + diff --git a/app/fr/maif/izanami/utils/datastore.scala b/app/fr/maif/izanami/utils/datastore.scala new file mode 100644 index 000000000..bc9db2788 --- /dev/null +++ b/app/fr/maif/izanami/utils/datastore.scala @@ -0,0 +1,22 @@ +package fr.maif.izanami.utils + +import akka.http.scaladsl.util.FastFuture +import fr.maif.izanami.env.Env +import play.api.Logger + +import scala.concurrent.{ExecutionContext, Future} + +trait Datastore { + implicit val ec: ExecutionContext = env.executionContext + protected val logger = Logger("izanami-datastore") + + def env: Env + + def onStart(): Future[Unit] = { + FastFuture.successful(()) + } + + def onStop(): Future[Unit] = { + FastFuture.successful(()) + } +} diff --git a/app/fr/maif/izanami/utils/syntax/syntax.scala b/app/fr/maif/izanami/utils/syntax/syntax.scala new file mode 100644 index 000000000..35b3d819d --- /dev/null +++ b/app/fr/maif/izanami/utils/syntax/syntax.scala @@ -0,0 +1,136 @@ +package fr.maif.izanami.utils.syntax + +import akka.NotUsed +import akka.http.scaladsl.util.FastFuture +import akka.stream.scaladsl.Source +import akka.util.ByteString +import org.apache.commons.codec.binary.Hex +import play.api.Logger +import play.api.libs.json._ + +import java.nio.charset.StandardCharsets +import java.security.MessageDigest +import scala.concurrent.Future + +object implicits { + implicit class BetterSyntax[A](private val obj: A) extends AnyVal { + @inline + def vfuture: Future[A] = FastFuture.successful(obj) + def seq: Seq[A] = Seq(obj) + def set: Set[A] = Set(obj) + def list: List[A] = List(obj) + def some: Option[A] = Some(obj) + def option: Option[A] = Some(obj) + def left[B]: Either[A, B] = Left(obj) + def right[B]: Either[B, A] = Right(obj) + def future: Future[A] = FastFuture.successful(obj) + def asFuture: Future[A] = FastFuture.successful(obj) + def toFuture: Future[A] = FastFuture.successful(obj) + def somef: Future[Option[A]] = FastFuture.successful(Some(obj)) + def leftf[B]: Future[Either[A, B]] = FastFuture.successful(Left(obj)) + def rightf[B]: Future[Either[B, A]] = FastFuture.successful(Right(obj)) + def debug(f: A => Any): A = { + f(obj) + obj + } + def debugPrintln: A = { + println(obj) + obj + } + def debugLogger(logger: Logger): A = { + logger.debug(s"$obj") + obj + } + def applyOn[B](f: A => B): B = f(obj) + def applyOnIf(predicate: => Boolean)(f: A => A): A = if (predicate) f(obj) else obj + def applyOnWithOpt[B](opt: => Option[B])(f: (A, B) => A): A = if (opt.isDefined) f(obj, opt.get) else obj + def applyOnWithPredicate(predicate: A => Boolean)(f: A => A): A = if (predicate(obj)) f(obj) else obj + + def seffectOn(f: A => Unit): A = { + f(obj) + obj + } + def seffectOnIf(predicate: => Boolean)(f: A => Unit): A = { + if (predicate) { + f(obj) + obj + } else obj + } + def seffectOnWithPredicate(predicate: A => Boolean)(f: A => Unit): A = { + if (predicate(obj)) { + f(obj) + obj + } else obj + } + } + + implicit class BetterListEither[E,V](private val obj: Iterable[Either[E, V]]) extends AnyVal { + def toEitherList: Either[List[E], List[V]] = { + obj.foldLeft(Right(List()):Either[List[E], List[V]]){ + case (Left(errors), Left(error)) => Left(errors.appended(error)) + case (l@Left(_), _) => l + case (Right(values), Right(value)) => Right(values.appended(value)) + case (_, Left(error)) => Left(List(error)) + } + } + } + + implicit class BetterJsValue(private val obj: JsValue) extends AnyVal { + def stringify: String = Json.stringify(obj) + def prettify: String = Json.prettyPrint(obj) + def select(name: String): JsLookupResult = obj \ name + def select(index: Int): JsLookupResult = obj \ index + def at(path: String): JsLookupResult = { + val parts = path.split("\\.").toSeq + parts.foldLeft(obj) { + case (source: JsObject, part) => (source \ part).as[JsValue] + case (source: JsArray, part) => (source \ part.toInt).as[JsValue] + case (value, part) => JsNull + } match { + case JsNull => JsUndefined(s"path '${path}' does not exists") + case value => JsDefined(value) + } + } + def atPointer(path: String): JsLookupResult = { + val parts = path.split("/").toSeq.filterNot(_.trim.isEmpty) + parts.foldLeft(obj) { + case (source: JsObject, part) => (source \ part).as[JsValue] + case (source: JsArray, part) => (source \ part.toInt).as[JsValue] + case (value, part) => JsNull + } match { + case JsNull => JsUndefined(s"path '${path}' does not exists") + case value => JsDefined(value) + } + } + def vertxJsValue: Object = io.vertx.core.json.Json.decodeValue(Json.stringify(obj)) + } + + implicit class BetterByteString(private val obj: ByteString) extends AnyVal { + def chunks(size: Int): Source[ByteString, NotUsed] = Source(obj.grouped(size).toList) + + def sha256: String = Hex.encodeHexString(MessageDigest.getInstance("SHA-256").digest(obj.toArray)) + + def sha512: String = Hex.encodeHexString(MessageDigest.getInstance("SHA-512").digest(obj.toArray)) + } + + implicit class BetterString(private val obj: String) extends AnyVal { + def byteString: ByteString = ByteString(obj) + + def bytes: Array[Byte] = obj.getBytes(StandardCharsets.UTF_8) + + def json: JsValue = JsString(obj) + + def parseJson: JsValue = Json.parse(obj) + + def camelToSnake: String = { + obj.replaceAll("([a-z])([A-Z]+)", "$1_$2").toLowerCase + // obj.replaceAll("([A-Z]+)([A-Z][a-z])", "$1_$2").replaceAll("([a-z])([A-Z])", "$1_$2").toLowerCase + } + + def sha512: String = Hex.encodeHexString( + MessageDigest.getInstance("SHA-512").digest(obj.getBytes(StandardCharsets.UTF_8)) + ) + + def sha256: String = Hex.encodeHexString(MessageDigest.getInstance("SHA-256").digest(obj.getBytes(StandardCharsets.UTF_8))) + } +} diff --git a/app/fr/maif/izanami/v1/OldFeatures.scala b/app/fr/maif/izanami/v1/OldFeatures.scala new file mode 100644 index 000000000..e79163726 --- /dev/null +++ b/app/fr/maif/izanami/v1/OldFeatures.scala @@ -0,0 +1,353 @@ +package fr.maif.izanami.v1 + +import fr.maif.izanami.models.{AbstractFeature, ActivationCondition, ActivationRule, All, DateRangeActivationCondition, Feature, FeaturePeriod, HourPeriod, SingleConditionFeature, UserList, UserPercentage, WasmFeature, ZonedHourPeriod} +import fr.maif.izanami.utils.syntax.implicits.BetterSyntax +import fr.maif.izanami.v1.OldFeatureType.{CUSTOMERS_LIST, DATE_RANGE, GLOBAL_SCRIPT, HOUR_RANGE, NO_STRATEGY, PERCENTAGE, RELEASE_DATE, SCRIPT} +import fr.maif.izanami.wasm.WasmConfig +import fr.maif.izanami.web.ImportController.scriptIdToNodeCompatibleName +import io.otoroshi.wasm4s.scaladsl.WasmSource +import io.otoroshi.wasm4s.scaladsl.WasmSourceKind.Wasmo +import play.api.libs.functional.syntax.{toApplicativeOps, toFunctionalBuilderOps} +import play.api.libs.json.Reads.{localDateTimeReads, localTimeReads, max, min} +import play.api.libs.json.{JsError, JsNull, JsObject, JsResult, JsSuccess, JsValue, Json, Reads, Writes, __} + +import java.time.format.DateTimeFormatter +import java.time.{LocalDateTime, LocalTime, ZoneId} + + +sealed trait OldFeature { + def id: String + def name: String + def enabled: Boolean + def description: Option[String] + def tags: Set[String] + def toFeature(project: String, zone: ZoneId, globalScriptById: Map[String, OldGlobalScript]): Either[String, (AbstractFeature, Option[OldScript])] = { + this match { + case OldDefaultFeature(id, name, enabled, description, tags, _) => Right((SingleConditionFeature(id=id, name=Option(name).getOrElse(id), enabled=enabled, project=project, condition=All, description=description.getOrElse(""), tags=tags), None)) + case OldDateRangeFeature(id, name, enabled, description, tags, from, to, _) => Right((SingleConditionFeature(id=id, name=Option(name).getOrElse(id), enabled=enabled, project=project, condition=DateRangeActivationCondition(begin = Option(from.atZone(zone).toInstant), end = Option(to.atZone(zone).toInstant), timezone = zone + ), description=description.getOrElse(""), tags=tags), None)) + case OldReleaseDateFeature(id, name, enabled, description, tags, date, _) => Right((SingleConditionFeature(id = id, name = Option(name).getOrElse(id), enabled = enabled, project = project, condition = + DateRangeActivationCondition(begin = Option(date.atZone(zone).toInstant), timezone = zone), description = description.getOrElse(""), tags=tags), None)) + case OldHourRangeFeature(id, name, enabled, description, tags, startAt, endAt, _) => Right((SingleConditionFeature(id = id, name = Option(name).getOrElse(id), enabled = enabled, project = project, condition = ZonedHourPeriod(timezone=zone, hourPeriod=HourPeriod(startTime = startAt, endTime = endAt)), description = description.getOrElse(""), tags=tags), None)) + case OldPercentageFeature(id, name, enabled, description, tags, percentage) => Right((SingleConditionFeature(id = id, name = Option(name).getOrElse(id), enabled = enabled, project = project, condition = UserPercentage(percentage = percentage), description = description.getOrElse(""), tags=tags), None)) + case OldCustomersFeature(id, name, enabled, description, tags, customers) => Right((SingleConditionFeature(id = id, name = Option(name).getOrElse(id), enabled = enabled, project = project, condition = UserList(users=customers.toSet), description = description.getOrElse(""), tags=tags), None)) + case OldScriptFeature(id, name, enabled, description, tags, script) => + Right(( + WasmFeature( + id = id, + name = Option(name).getOrElse(id), + project = project, + enabled = enabled, + description = description.getOrElse(""), + tags=tags, + wasmConfig = WasmConfig( + name = s"${id}_script", + // TODO release script & remove -dev + source = WasmSource(kind = null, path = "TODO", opts = Json.obj()), + functionName = Some("execute"), + wasi = true + ) + ), + Some(script) + )) + case OldGlobalScriptFeature(id, name, enabled, description, tags, ref) => { + globalScriptById.get(scriptIdToNodeCompatibleName(ref)) match { + case None => Left(s"Can't find referenced global script ${ref}") + case Some(OldGlobalScript(scriptId, scriptName, scriptDescription, source)) => { + Right((WasmFeature( + id = id, + name = Option(name).getOrElse(id), + project = project, + enabled = enabled, + description = description.getOrElse(""), + tags=tags, + wasmConfig = WasmConfig( + name = scriptId, + // TODO release script & remove -dev + source = WasmSource(kind = Wasmo, path = "TODO", opts = Json.obj()), + functionName = Some("execute"), + wasi = true + ) + ), None)) + } + } + } + } + } +} + +sealed trait OldPluginLangage +case object JavaScript extends OldPluginLangage +case object Kotlin extends OldPluginLangage +case object Scala extends OldPluginLangage + +case class OldDefaultFeature(id: String, name: String, enabled: Boolean, description: Option[String], tags: Set[String], parameters: JsValue = JsNull) + extends OldFeature + + +case class OldScript(language: OldPluginLangage, script: String) +case class OldGlobalScript(id: String, name: String, description: Option[String], source: OldScript) + +case class OldGlobalScriptFeature(id: String, name: String, enabled: Boolean, description: Option[String], tags: Set[String], ref: String) + extends OldFeature + +case class OldScriptFeature(id: String, name: String, enabled: Boolean, description: Option[String], tags: Set[String], script: OldScript) + extends OldFeature + +case class OldDateRangeFeature( + id: String, + name: String, + enabled: Boolean, + description: Option[String], tags: Set[String], + from: LocalDateTime, + to: LocalDateTime, + timezone: ZoneId = null + ) extends OldFeature + +case class OldReleaseDateFeature(id: String, name: String, enabled: Boolean, description: Option[String], tags: Set[String], releaseDate: LocalDateTime, timezone: ZoneId = null) + extends OldFeature + +case class OldHourRangeFeature( + id: String, + name: String, + enabled: Boolean, + description: Option[String], tags: Set[String], + startAt: LocalTime, + endAt: LocalTime, + timezone: ZoneId = null + ) extends OldFeature + +case class OldPercentageFeature(id: String, name: String, enabled: Boolean, description: Option[String], tags: Set[String], percentage: Int) + extends OldFeature + +case class OldCustomersFeature( + id: String, + name: String, + enabled: Boolean, + description: Option[String], tags: Set[String], + customers: List[String] + ) extends OldFeature + +object OldFeatureType { + val NO_STRATEGY = "NO_STRATEGY" + val RELEASE_DATE = "RELEASE_DATE" + val DATE_RANGE = "DATE_RANGE" + val SCRIPT = "SCRIPT" + val GLOBAL_SCRIPT = "GLOBAL_SCRIPT" + val PERCENTAGE = "PERCENTAGE" + val HOUR_RANGE = "HOUR_RANGE" + val CUSTOMERS_LIST = "CUSTOMERS_LIST" +} + + +object OldFeature { + private val timePattern = "HH:mm" + private val timePattern2 = "H:mm" + private val dateTimePattern = "dd/MM/yyyy HH:mm:ss" + private val dateTimePattern2 = "dd/MM/yyyy HH:mm" + private val dateTimePattern3 = "yyyy-MM-dd HH:mm:ss" + + + def fromModernFeature(f: SingleConditionFeature): OldFeature = { + f.condition match { + case DateRangeActivationCondition(begin, end, timezone) => { + {for( + b <- begin; + e <- end + ) yield OldDateRangeFeature(id = f.id, name=f.name, enabled = f.enabled, description = Option(f.description), from = b.atZone(timezone).toLocalDateTime, to = e.atZone(timezone).toLocalDateTime, timezone = timezone, tags=f.tags) + }.orElse(begin.map(b => OldReleaseDateFeature(id = f.id, name=f.name, enabled = f.enabled, description = Option(f.description), releaseDate=b.atZone(timezone).toLocalDateTime, timezone = timezone, tags=f.tags))) + .getOrElse(throw new RuntimeException("Failed to convert SingleConditionFeature to OldFeature, this should not happen, please file an issue")) + } + case ZonedHourPeriod(HourPeriod(start, end), timezone) => OldHourRangeFeature(id=f.id, name=f.name, enabled=f.enabled, description = Option(f.description), startAt = start, endAt = end, timezone=timezone, tags=f.tags) + case UserPercentage(percentage) => OldPercentageFeature(id=f.id, name=f.name, enabled = f.enabled, description = Some(f.description), percentage = percentage, tags=f.tags) + case UserList(users) => OldCustomersFeature(id=f.id, name=f.name, enabled = f.enabled, description = Option(f.description), customers = users.toList, tags=f.tags) + case All => OldDefaultFeature(f.id, f.name, f.enabled, description = Option(f.description), parameters = Json.obj(), tags=f.tags) + } + } + + def fromScriptFeature(f: WasmFeature): OldFeature = { + OldGlobalScriptFeature(f.id, name = f.name, enabled = f.enabled, description = Some(f.description), tags = f.tags, ref = f.wasmConfig.name) + } + + def commonRead = + (__ \ "id").read[String] and + (__ \ "name").readWithDefault[String]("null") and + (__ \ "enabled").read[Boolean].orElse(Reads.pure(false)) and + (__ \ "description").readNullable[String] and + (__ \ "tags").readWithDefault[Set[String]](Set[String]()) + + + implicit val percentageReads: Reads[OldPercentageFeature] = ( + commonRead and + (__ \ "parameters" \ "percentage").read[Int](min(0) keepAnd max(100)) + )(OldPercentageFeature.apply _) + + implicit val customerReads: Reads[OldCustomersFeature] = ( + commonRead and + (__ \ "parameters" \ "customers").read[List[String]] + )(OldCustomersFeature.apply _) + + implicit val hourRangeReads: Reads[OldHourRangeFeature] = ( + commonRead and + (__ \ "parameters" \ "startAt") + .read[LocalTime](localTimeReads(timePattern2)) + .orElse(localTimeReads(timePattern)) and + (__ \ "parameters" \ "endAt") + .read[LocalTime](localTimeReads(timePattern2)) + .orElse(localTimeReads(timePattern)) + )((id, name, enabled, description, tags, start, end) => OldHourRangeFeature(id, name, enabled, description, tags, start, end)) + + + implicit val defaultFeatureReads: Reads[OldDefaultFeature] = ( + commonRead and + (__ \ "parameters").readNullable[JsValue].map(_.getOrElse(JsNull)) + )(OldDefaultFeature.apply _) + + + implicit val globalScriptFeatureReads: Reads[OldGlobalScriptFeature] = ( + commonRead and + (__ \ "parameters" \ "ref").read[String] + )(OldGlobalScriptFeature.apply _) + + implicit val langageRead: Reads[OldPluginLangage] = json => { + json.asOpt[String].map(_.toUpperCase).map(l => l match { + case "JAVASCRIPT" => JsSuccess(JavaScript) + case "SCALA" => JsSuccess(Scala) + case "KOTLIN" => JsSuccess(Kotlin) + case _ => JsError(s"Unknown plugin langage $l") + }).getOrElse( JsError(s"Missing langage for plugin")) + } + + implicit val localScriptRead: Reads[OldScript] = ( + (__ \ "type").read[OldPluginLangage] and + (__ \ "script").read[String] + )(OldScript.apply _) + + implicit val scriptFeatureReads: Reads[OldScriptFeature] = ( + commonRead and + (__ \ "parameters").read[OldScript](localScriptRead) + )(OldScriptFeature.apply _) + + implicit val globalScriptReads: Reads[OldGlobalScript] = ( + (__ \ "id").read[String] and + (__ \ "name").read[String] and + (__ \ "description").readNullable[String] and + (__ \ "source").read[OldScript](localScriptRead) + )(OldGlobalScript.apply _) + + + implicit val dateRangeReads: Reads[OldDateRangeFeature] = ( + commonRead and + (__ \ "parameters" \ "from") + .read[LocalDateTime](localDateTimeReads(dateTimePattern3)) and + (__ \ "parameters" \ "to") + .read[LocalDateTime](localDateTimeReads(dateTimePattern3)) + )((id, name, enabled, description, tags, from, to) => OldDateRangeFeature(id=id, name=name, enabled=enabled, description=description, tags=tags, from=from, to=to)) + + implicit val releaseDateReads: Reads[OldReleaseDateFeature] = ( + commonRead and + (__ \ "parameters" \ "releaseDate") + .read[LocalDateTime]( + localDateTimeReads(dateTimePattern).orElse(localDateTimeReads(dateTimePattern2)).orElse(localDateTimeReads(dateTimePattern3)) + ) + )((id, name, enabled, description, tags, releaseDate) => OldReleaseDateFeature(id=id, name=name, enabled=enabled, description=description, releaseDate=releaseDate, tags=tags)) + + val oldFeatureReads: Reads[OldFeature] = {json => + (json \ "activationStrategy").asOpt[String].map { + case NO_STRATEGY => JsSuccess(json.as[OldDefaultFeature]) + case RELEASE_DATE => JsSuccess(json.as[OldReleaseDateFeature]) + case DATE_RANGE => JsSuccess(json.as[OldDateRangeFeature]) + case SCRIPT => JsSuccess(json.as[OldScriptFeature]) + case GLOBAL_SCRIPT => JsSuccess(json.as[OldGlobalScriptFeature]) + case PERCENTAGE => JsSuccess(json.as[OldPercentageFeature]) + case HOUR_RANGE => JsSuccess(json.as[OldHourRangeFeature]) + case CUSTOMERS_LIST => JsSuccess(json.as[OldCustomersFeature]) + case _ => JsError("Bad feature strategy") + }.getOrElse(JsError("Bad feature format")) + } + + def commonWrite(feature: OldFeature): JsObject = { + Json.obj( + "id" -> feature.id, + "name" -> feature.name, + "enabled" -> feature.enabled, + "tags" -> feature.tags + ).applyOnWithOpt(feature.description)((json, desc) => json ++ Json.obj("description" -> desc)) + } + + implicit val oldDefaultFeatureWrites: Writes[OldDefaultFeature] = feature => { + commonWrite(feature) ++ Json.obj( + "activationStrategy" -> NO_STRATEGY, + "parameters" -> Json.obj() + ) + } + + implicit val oldGlobalScriptWrites: Writes[OldGlobalScriptFeature] = feature => { + commonWrite(feature) ++ Json.obj( + "activationStrategy" -> GLOBAL_SCRIPT, + "parameters" -> Json.obj("ref" -> feature.ref) + ) + } + + implicit val oldPercentageFeatureWrites: Writes[OldPercentageFeature] = feature => { + commonWrite(feature) ++ Json.obj( + "activationStrategy" -> PERCENTAGE, + "parameters" -> Json.obj( + "percentage" -> feature.percentage + ) + ) + } + + implicit val oldCustomerFeatureWrites: Writes[OldCustomersFeature] = feature => { + commonWrite(feature) ++ Json.obj( + "activationStrategy" -> CUSTOMERS_LIST, + "parameters" -> Json.obj( + "customers" -> feature.customers + ) + ) + } + + val dateFormatter = DateTimeFormatter.ofPattern(dateTimePattern3) + + implicit val oldReleaseDateFeatureWrites: Writes[OldReleaseDateFeature] = feature => { + commonWrite(feature) ++ Json.obj( + "activationStrategy" -> RELEASE_DATE, + "parameters" -> Json.obj( + "releaseDate" -> feature.releaseDate.format(dateFormatter) + ) + ) + } + + implicit val oldDateRangeFeatureWrites: Writes[OldDateRangeFeature] = feature => { + commonWrite(feature) ++ Json.obj( + "activationStrategy" -> DATE_RANGE, + "parameters" -> Json.obj( + "from" -> feature.from.format(dateFormatter), + "to" -> feature.to.format(dateFormatter) + ) + ) + } + + val timeFormatter = DateTimeFormatter.ofPattern("HH:mm") + + implicit val oldHourRangeWrites: Writes[OldHourRangeFeature] = feature => { + commonWrite(feature) ++ Json.obj( + "activationStrategy" -> HOUR_RANGE, + "parameters" -> Json.obj( + "startAt" -> feature.startAt.format(timeFormatter), + "endAt" -> feature.endAt.format(timeFormatter) + ) + ) + } + + val oldFeatureWrites: Writes[OldFeature] = { + case f: OldDefaultFeature => oldDefaultFeatureWrites.writes(f) + case f: OldGlobalScriptFeature => oldGlobalScriptWrites.writes(f) + case f: OldScriptFeature => throw new RuntimeException("Failed to write OldGlobalScriptFeature, this should no happen") + case f: OldDateRangeFeature => oldDateRangeFeatureWrites.writes(f) + case f: OldReleaseDateFeature => oldReleaseDateFeatureWrites.writes(f) + case f: OldHourRangeFeature => oldHourRangeWrites.writes(f) + case f: OldPercentageFeature => oldPercentageFeatureWrites.writes(f) + case f: OldCustomersFeature => oldCustomerFeatureWrites.writes(f) + } +} \ No newline at end of file diff --git a/app/fr/maif/izanami/v1/OldKeys.scala b/app/fr/maif/izanami/v1/OldKeys.scala new file mode 100644 index 000000000..853e6955b --- /dev/null +++ b/app/fr/maif/izanami/v1/OldKeys.scala @@ -0,0 +1,49 @@ +package fr.maif.izanami.v1 + +import fr.maif.izanami.models.{ApiKey, RightLevels} +import fr.maif.izanami.v1.OldCommons.{authorizedPatternReads, filterProjects, oldRightToNewRight, toNewRights} +import play.api.libs.functional.syntax.toFunctionalBuilderOps +import play.api.libs.json.{Reads, __} + +case class OldKey( + clientId: String, + name: String, + clientSecret: String, + authorizedPatterns: Seq[AuthorizedPattern], + admin: Boolean = false +) + +object OldKey { + def toNewKey(tenant: String, key: OldKey, projects: Set[String], shoudlFilterProject: Boolean): ApiKey = { + val filteredProjects = if(shoudlFilterProject){ + key.authorizedPatterns.flatMap(ap => { + filterProjects(ap.pattern, projects) + }).toSet + } else { + projects + } + + ApiKey( + tenant = tenant, + clientId = key.clientId, + clientSecret = key.clientSecret, + name = key.name, + projects = filteredProjects, + enabled = true, + legacy = true, + admin=key.admin + ) + } + + val oldKeyReads: Reads[OldKey] = { + ( + (__ \ "clientId").read[String].filter(name => "^[@0-9\\p{L} .'-]+$".r.pattern.matcher(name).matches()) and + (__ \ "name").read[String].filter(name => "^[@0-9\\p{L} .'-]+$".r.pattern.matcher(name).matches()) and + (__ \ "clientSecret").read[String].filter(name => "^[@0-9\\p{L} .'-]+$".r.pattern.matcher(name).matches()) and + (__ \ "authorizedPatterns") + .read[Seq[AuthorizedPattern]] + .orElse((__ \ "authorizedPatterns").read[Seq[AuthorizedPattern]]) and + (__ \ "admin").read[Boolean].orElse(Reads.pure(false)) + )(OldKey.apply _) + } +} diff --git a/app/fr/maif/izanami/v1/OldUsers.scala b/app/fr/maif/izanami/v1/OldUsers.scala new file mode 100644 index 000000000..cf65dcb72 --- /dev/null +++ b/app/fr/maif/izanami/v1/OldUsers.scala @@ -0,0 +1,88 @@ +package fr.maif.izanami.v1 + +import fr.maif.izanami.models._ +import fr.maif.izanami.utils.syntax.implicits.BetterSyntax +import fr.maif.izanami.v1.OldCommons.{authorizedPatternReads, toNewRights} + +trait OldUser { + def email: String + def admin: Boolean + def authorizedPatterns: Seq[AuthorizedPattern] + +} + +case class OldIzanamiUser( + id: String, + name: String, + email: String, + password: Option[String], + admin: Boolean, + authorizedPatterns: Seq[AuthorizedPattern] +) extends OldUser + +case class OldOauthUser( + id: String, + name: String, + email: String, + admin: Boolean, + authorizedPatterns: Seq[AuthorizedPattern] +) extends OldUser {} + +case class OldOtoroshiUser( + id: String, + name: String, + email: String, + admin: Boolean, + authorizedPatterns: Seq[AuthorizedPattern] +) extends OldUser {} + +object OldUsers { + import play.api.libs.json._ + + def toNewUser(tenant: String, user: OldUser, projects: Set[String], filterProject: Boolean): Either[String, UserWithRights] = { + user match { + case OldIzanamiUser(id, name, email, password, admin, authorizedPatterns) => UserWithRights(username=id, userType = INTERNAL,email=email, password=password.orNull, admin=admin, rights = Rights(tenants=(Map(tenant -> toNewRights(admin, authorizedPatterns, projects, filterProject)))), legacy=true).right + case OldOauthUser(id, name, email, admin, authorizedPatterns) => UserWithRights(username=id, userType = OIDC,email=email, password=null, admin=admin, rights = Rights(tenants=(Map(tenant -> toNewRights(admin, authorizedPatterns, projects, filterProject)))), legacy=true).right + case OldOtoroshiUser(id, name, email, admin, authorizedPatterns) => UserWithRights(username=id, userType = OTOROSHI,email=email, password=null, admin=admin, rights = Rights(tenants=(Map(tenant -> toNewRights(admin, authorizedPatterns, projects, filterProject)))), legacy=true).right + case _ => Left(s"Incorrect user type for ${user}") + } + } + + + // TODO handle pattern + val oldUserReads: Reads[OldUser] = { + import play.api.libs.functional.syntax._ + import play.api.libs.json._ + val commonReads = + (__ \ "id").read[String] and + (__ \ "name").read[String] and + (__ \ "email").read[String] and + (__ \ "admin").read[Boolean] and + (__ \ "authorizedPatterns").read[Seq[AuthorizedPattern]].orElse((__ \ "authorizedPattern").read[Seq[AuthorizedPattern]]) + + val readOtoroshiUser = commonReads(OldOtoroshiUser.apply _) + val readOauthUser = commonReads(OldOauthUser.apply _) + + val readIzanamiUser = ( + (__ \ "id").read[String] and + (__ \ "name").read[String] and + (__ \ "email").read[String] and + (__ \ "password").readNullable[String] and + (__ \ "admin").read[Boolean] and + (__ \ "authorizedPatterns").read[Seq[AuthorizedPattern]].orElse((__ \ "authorizedPattern").read[Seq[AuthorizedPattern]]) + )(OldIzanamiUser.apply _) + + (__ \ "type").readNullable[String].flatMap { + case Some(UserType.Otoroshi) => readOtoroshiUser.asInstanceOf[Reads[OldUser]] + case Some(UserType.Oauth) => readOauthUser.asInstanceOf[Reads[OldUser]] + case Some(UserType.Izanami) => readIzanamiUser.asInstanceOf[Reads[OldUser]] + case _ => readIzanamiUser.asInstanceOf[Reads[OldUser]] + } + } +} + +object UserType { + val Otoroshi = "Otoroshi" + val Oauth = "OAuth" + val Izanami = "Izanami" +} \ No newline at end of file diff --git a/app/fr/maif/izanami/v1/V1FeatureEvents.scala b/app/fr/maif/izanami/v1/V1FeatureEvents.scala new file mode 100644 index 000000000..26e86f029 --- /dev/null +++ b/app/fr/maif/izanami/v1/V1FeatureEvents.scala @@ -0,0 +1,66 @@ +package fr.maif.izanami.v1 + +import fr.maif.izanami.models.AbstractFeature +import fr.maif.izanami.security.IdGenerator +import play.api.libs.json.{JsNull, JsObject, Json} + +import java.time.LocalDateTime + + +object V1FeatureEvents { + private val gen = IdGenerator(1024) + private def baseJson(id: String, feature: JsObject): JsObject = { + Json.obj( + "_id" -> gen.nextId(), + "domain" -> "Feature", + "timestamp" -> LocalDateTime.now(), + "key" -> id, + "payload" -> feature + ) + } + + def updateEvent(id: String, feature: JsObject): JsObject = { + baseJson(id, feature) ++ Json.obj( + "type" -> "FEATURE_UPDATED", + "oldValue" -> feature + ) + } + + def createEvent(id: String, feature: JsObject): JsObject = { + baseJson(id, feature) ++ Json.obj( + "type" -> "FEATURE_CREATED" + ) + } + + def keepAliveEvent(): JsObject = { + Json.obj( + "_id" -> gen.nextId(), + "type" -> "KEEP_ALIVE", + "key" -> "na", + "domain" -> "Unknown", + "payload" -> Json.obj(), + "authInfo" -> JsNull, + "timestamp" -> LocalDateTime.now() + ) + } + + def deleteEvent(id: String): JsObject = { + Json.obj( + "_id" -> gen.nextId(), + "domain" -> "Feature", + "timestamp" -> LocalDateTime.now(), + "key" -> id, + "type" -> "FEATURE_DELETED", + "payload" -> Json.obj("id" -> id, "enabled" -> false) + ) + } + + private def writeFeatureForEvent(feature: AbstractFeature): JsObject = { + Json.obj( + "enabled" -> feature.enabled, + "id" -> feature.id, + "parameters" -> Json.obj(), + "activationStrategy" -> "NO_STRATEGY" + ) + } +} diff --git a/app/fr/maif/izanami/v1/commons.scala b/app/fr/maif/izanami/v1/commons.scala new file mode 100644 index 000000000..3138e93e1 --- /dev/null +++ b/app/fr/maif/izanami/v1/commons.scala @@ -0,0 +1,91 @@ +package fr.maif.izanami.v1 + +import fr.maif.izanami.models.RightLevels.RightLevel +import fr.maif.izanami.models.{AtomicRight, RightLevels, TenantRight} +import fr.maif.izanami.utils.syntax.implicits.BetterSyntax +import play.api.libs.json.{JsError, JsSuccess, Reads} + + +sealed trait Right +case object Read extends Right +case object Update extends Right +case object Create extends Right +case object Delete extends Right + +case class AuthorizedPattern(pattern: String, rights: Seq[Right]) + + +object OldCommons { + def toNewRights(admin: Boolean, patterns: Seq[AuthorizedPattern], projects: Set[String], shouldFilterProjects: Boolean): TenantRight = { + // TODO handle wildcard in rights (by allowing to read db with regexp ? if so features must be inserted first) + val projectRights = patterns.map(ap => { + for ( + filteredProjects <- if(shouldFilterProjects) filterProjects(ap.pattern, projects) else projects; + right <- oldRightToNewRight(ap.rights.toSet) + ) yield (filteredProjects, right) + }).flatMap(_.toSeq).groupBy { case (key, _) => key } + .view.mapValues(s => s.map{case (_, level) => level} + .reduce((r1, r2) => if(RightLevels.superiorOrEqualLevels(r1).contains(r2)) r2 else r1)) + .view.mapValues(AtomicRight) + + val tenantRight = if(admin) { + RightLevels.Admin + } else if(patterns.map(p => p.pattern).contains("*")) { + RightLevels.Write + } else { + RightLevels.Read + } + + TenantRight(level=tenantRight, projects = projectRights.toMap) + } + + def filterProjects(pattern: String, projects: Set[String]): Set[String] = { + if(pattern.trim == "*") { + projects + } else if(pattern.contains("*")) { + var regexp = pattern.replace("*", ".*") + if(regexp.endsWith(":.*")) { + regexp = regexp.dropRight(3).concat("(:.*)?") + } + projects.filter(p => regexp.r.matches(p)) + } else { + // TODO people with right on only one feature should have right on create project if feature is alone in it + Set() + } + } + + implicit val rightReads: Reads[Right] = { json => + (for ( + str <- json.asOpt[String]; + if (str.length == 1); + c <- str.charAt(0).toUpper.option + ) yield c match { + case 'R' => JsSuccess(Read) + case 'U' => JsSuccess(Update) + case 'C' => JsSuccess(Create) + case 'D' => JsSuccess(Delete) + case _ => JsError(s"Unknown right level letter : ${c}") + }).getOrElse(JsError(s"Right string too long ${json}")) + } + + def oldRightToNewRight(right: Set[Right]): Option[RightLevel] = { + val OLD_ALL_RIGHTS: Set[Right] = Set(Read, Update, Delete, Create) + + if (right.isEmpty) { + None + } else if (OLD_ALL_RIGHTS.subsetOf(right)) { + RightLevels.Admin.some + } else if (right.contains(Read) && right.size == 1) { + RightLevels.Read.some + } else { + RightLevels.Write.some + } + } + + implicit val authorizedPatternReads: Reads[AuthorizedPattern] = { json => + (for ( + pattern <- (json \ "pattern").asOpt[String]; + rights <- (json \ "rights").asOpt[Seq[Right]] + ) yield JsSuccess(AuthorizedPattern(pattern = pattern, rights = rights))).getOrElse(JsError("Bad right format")) + } +} \ No newline at end of file diff --git a/app/fr/maif/izanami/v1/scripts.scala b/app/fr/maif/izanami/v1/scripts.scala new file mode 100644 index 000000000..9e61ff3eb --- /dev/null +++ b/app/fr/maif/izanami/v1/scripts.scala @@ -0,0 +1,263 @@ +package fr.maif.izanami.v1 + +import akka.util.ByteString +import fr.maif.izanami.env.Env +import fr.maif.izanami.errors.{ConfigurationReadError, IzanamiError, NoWasmManagerConfigured} +import fr.maif.izanami.models.IzanamiConfiguration +import fr.maif.izanami.v1.OldScripts.generateNewScriptContent +import io.otoroshi.wasm4s.scaladsl.{ApikeyHelper, WasmoSettings} +import org.mozilla.javascript.Parser +import org.mozilla.javascript.ast._ +import play.api.libs.json.{JsBoolean, Json} +import play.api.libs.ws.WSClient + +import java.time.Duration +import scala.concurrent.{ExecutionContext, Future} + +class WasmManagerClient(env: Env)(implicit ec: ExecutionContext) { + val httpClient: WSClient = env.Ws + + def wasmoConfiguration: Option[WasmoSettings] = env.datastores.configuration.readWasmConfiguration() + + def transferLegacyJsScript(name: String, content: String, local: Boolean): Future[Either[IzanamiError, (String, ByteString)]] = { + wasmoConfiguration match { + case None => Future.successful(Left(NoWasmManagerConfigured())) + case Some(w:WasmoSettings) => { + createScript( + name, + generateNewScriptContent(content), + config = w, + local = local + ).map(t => Right(t)) + } + } + } + + + def createScript( + name: String, + content: String, + config: WasmoSettings, + local: Boolean + ): Future[(String, ByteString)] = { + if(local) { + build(config, name, content) + .map(queueId => queueId) + .flatMap(queueId => retrieveLocallyBuildedWasm(s"$name-1.0.0-dev", config).map(bs => (queueId, bs))) + } else { + val pluginName = s"$name-1.0.0" + for ( + pluginId <- httpClient.url(s"${config.url}/api/plugins") + .withHttpHeaders(("Content-Type", "application/json"), ApikeyHelper.generate(config)) + .post(Json.obj( + "metadata" -> Json.obj( + "type" -> "js", + "name" -> name, + "release" -> true, + "local" -> false + ), + "files" -> Json.arr(Json.obj( + "name" -> "index.js", "content" -> content, + + ), Json.obj("name" -> "plugin.d.ts", "content" -> + """declare module 'main' { + | export function execute(): I32; + |}""".stripMargin)) + )) + .map(response => (response.json \ "plugin_id").as[String]); + _ <- buildAndSave(pluginId, config=config); + wasm <- retrieveBuildedWasm(pluginName, config) + ) yield { + (pluginName, wasm) + } + } + } + + def buildAndSave( + pluginId: String, + config: WasmoSettings, + ): Future[String] = { + httpClient + .url(s"${config.url}/api/plugins/$pluginId/build?release=true") + .withHttpHeaders(ApikeyHelper.generate(config)) + .post("") + .map(response => (response.json \ "queue_id").as[String]) + } + + def build( + config: WasmoSettings, + name: String, + content: String + ): Future[String] = { + httpClient + .url(s"${config.url}/api/plugins/build") + .withHttpHeaders(("Content-Type", "application/json")) + .withHttpHeaders(ApikeyHelper.generate(config)) + .post( + Json.obj( + "metadata" -> Json.obj( + "type" -> "js", + "name" -> name, + "version" -> "1.0.0", + "local" -> true, + "release" -> false + ), + "files" -> Json.arr( + Json.obj("name" -> "index.js" , "content" -> content), + Json.obj("name" -> "plugin.d.ts", "content" -> + """declare module 'main' { + | export function execute(): I32; + |}""".stripMargin), + Json.obj("name" -> "package.json", "content" -> + s""" + { + "name": "$name", + "version": "1.0.0", + "devDependencies": { + "esbuild": "^0.17.9" + } + } + """.stripMargin) + ) + ) + ) + .map(response => (response.json \ "queue_id").as[String]) + } + + private def retrieveBuildedWasm(name: String, config: WasmoSettings): Future[ByteString] = { + tryUntil( + () => { + httpClient + .url(s"${config.url}/api/wasm/$name") + .withHttpHeaders(ApikeyHelper.generate(config)) + .get() + .map(resp => if (resp.status >= 400) throw new RuntimeException("Bad status") else resp.bodyAsBytes) + }, + Duration.ofMinutes(1) + ) + } + + private def retrieveLocallyBuildedWasm(id: String, config: WasmoSettings): Future[ByteString] = { + tryUntil( + () => { + httpClient + .url(s"${config.url}/local/wasm/$id") + .withHttpHeaders(ApikeyHelper.generate(config)) + .get() + .map(resp => if (resp.status >= 400) throw new RuntimeException("Bad status") else resp.bodyAsBytes) + }, + Duration.ofMinutes(1) + ) + } + + def tryUntil[T](op: () => Future[T], duration: Duration): Future[T] = { + val start = System.currentTimeMillis() + + def act(count: Int = 0): Future[T] = { + if (Duration.ofMillis(System.currentTimeMillis() - start).compareTo(duration) > 0) { + throw new RuntimeException(s"Failed attempted operation, tried ${count} times") + } + op().recoverWith(ex => { + Thread.sleep(1000L) + act(count + 1) + }) + } + + act(0) + } +} + +object OldScripts { + // TODO change execute name to something with less collision risks + def generateNewScriptContent(oldScript: String): String = { + s""" + |export function execute() { + | let context = JSON.parse(Host.inputString()); + | + | let enabledCallback = () => Host.outputString(JSON.stringify(true)); + | let disabledCallback = () => Host.outputString(JSON.stringify(false)); + | + | enabled(context, enabledCallback, disabledCallback); + | + | return 0; + |} + | + |${oldScript} + |""".stripMargin + } + + def doesUseHttp(script: String): Boolean = { + val astRoot = new Parser().parse(script, "tmp.js", 1) + var paramName: Option[String] = None + var hasFunctionEnoughParameters = true + var foundUsage = false + var shouldSkipFirst = true + astRoot.visitAll(new NodeVisitor() { + override def visit(node: AstNode): Boolean = { + node match { + case node: FunctionNode => { + val params = node.getParams + if (node.getName == "enabled" && (node.getParent == null || node.getParent.getParent == null)) { + hasFunctionEnoughParameters = params.size() >= 4 + if (params.size() >= 4 && params.get(3).isInstanceOf[Name]) { + val scope = params.get(3).asInstanceOf[Name].getDefiningScope + paramName = Some(params.get(3).asInstanceOf[Name].getIdentifier) + } + + } + } + case fc: FunctionCall => { + if (fc.getTarget.isInstanceOf[Name]) { + val name = fc.getTarget.asInstanceOf[Name].getIdentifier + val nameMatch = paramName.contains(name) + if (nameMatch) { + foundUsage = true + } + } + } + case es: ExpressionStatement => { + es.getExpression.visit(this) + } + case name: Name => { + foundUsage = paramName + .map(n => n == name.getIdentifier) + .map(res => { + if (res && shouldSkipFirst) { + shouldSkipFirst = false + foundUsage + } else { + res + } + }) + .getOrElse(foundUsage) + } + case s @ _ => () + } + hasFunctionEnoughParameters && !foundUsage + } + }) + foundUsage + } + + def findUsageOf(node: AstNode, name: String): Boolean = { + var found = false + node.visit(new NodeVisitor() { + override def visit(node: AstNode): Boolean = { + node match { + case fc: FunctionCall => { + fc.getArguments + .stream() + .filter(n => n.isInstanceOf[Name]) + .map(n => n.asInstanceOf[Name]) + .filter(n => n.getIdentifier == name) + .findFirst() + .ifPresent(_ => found = true) + } + case _ => {} + } + true + } + }) + found + } +} diff --git a/app/fr/maif/izanami/wasm/host.scala b/app/fr/maif/izanami/wasm/host.scala new file mode 100644 index 000000000..d17cd5c90 --- /dev/null +++ b/app/fr/maif/izanami/wasm/host.scala @@ -0,0 +1,190 @@ +package fr.maif.izanami.wasm.host.scala + +import akka.http.scaladsl.model.Uri +import akka.stream.Materializer +import akka.util.ByteString +import fr.maif.izanami.env.Env +import fr.maif.izanami.utils.syntax.implicits.{BetterJsValue, BetterSyntax} +import fr.maif.izanami.wasm.WasmConfig +import io.otoroshi.wasm4s.scaladsl.{EmptyUserData, EnvUserData, HostFunctionWithAuthorization} +import org.extism.sdk.{ExtismCurrentPlugin, ExtismFunction, HostFunction, HostUserData, LibExtism} +import org.extism.sdk.wasmotoroshi._ +import play.api.libs.json.{JsValue, Json} +import play.api.libs.typedmap.TypedMap + +import java.nio.charset.StandardCharsets +import java.util.Optional +import java.util.concurrent.TimeUnit +import scala.concurrent.duration.Duration +import scala.concurrent.{Await, ExecutionContext} + +object HFunction { + def defineContextualFunction( + fname: String, + config: WasmConfig + )( + f: (ExtismCurrentPlugin, Array[LibExtism.ExtismVal], Array[LibExtism.ExtismVal], EnvUserData) => Unit + )(implicit env: Env, ec: ExecutionContext, mat: Materializer): HostFunction[EnvUserData] = { + val ev = EnvUserData(env.wasmIntegration.context, ec, mat, config) + defineFunction[EnvUserData]( + fname, + ev.some, + LibExtism.ExtismValType.I64, + LibExtism.ExtismValType.I64, + LibExtism.ExtismValType.I64 + )((p1, p2, p3, _) => f(p1, p2, p3, ev)) + } + + def defineFunction[A <: EnvUserData]( + fname: String, + data: Option[A], + returnType: LibExtism.ExtismValType, + params: LibExtism.ExtismValType* + )( + f: (ExtismCurrentPlugin, Array[LibExtism.ExtismVal], Array[LibExtism.ExtismVal], Option[A]) => Unit + ): HostFunction[A] = { + new HostFunction[A]( + fname, + Array(params: _*), + Array(returnType), + new ExtismFunction[A] { + override def invoke( + plugin: ExtismCurrentPlugin, + params: Array[LibExtism.ExtismVal], + returns: Array[LibExtism.ExtismVal], + data: Optional[A] + ): Unit = { + f(plugin, params, returns, if (data.isEmpty) None else Some(data.get())) + } + }, + data match { + case None => Optional.empty[A]() + case Some(d) => Optional.of(d) + } + ) + } +} + +object Utils { + def rawBytePtrToString(plugin: ExtismCurrentPlugin, offset: Long, arrSize: Long): String = { + val memoryLength = plugin.memoryLength(arrSize) + val arr = plugin + .memory() + .share(offset, memoryLength) + .getByteArray(0, arrSize.toInt) + new String(arr, StandardCharsets.UTF_8) + } + + def contextParamsToString(plugin: ExtismCurrentPlugin, params: LibExtism.ExtismVal*) = { + rawBytePtrToString(plugin, params(0).v.i64, params(1).v.i32) + } + + def contextParamsToJson(plugin: ExtismCurrentPlugin, params: LibExtism.ExtismVal*) = { + Json.parse(rawBytePtrToString(plugin, params(0).v.i64, params(1).v.i32)) + } +} + +object HttpCall { + def proxyHttpCall(config: WasmConfig)(implicit env: Env, executionContext: ExecutionContext, mat: Materializer) = { + HFunction.defineContextualFunction("proxy_http_call", config) { + ( + plugin: ExtismCurrentPlugin, + params: Array[LibExtism.ExtismVal], + returns: Array[LibExtism.ExtismVal], + hostData: EnvUserData + ) => { + val context = Json.parse(Utils.contextParamsToString(plugin, params.toIndexedSeq:_*)) + + val url = (context \ "url").asOpt[String].getOrElse("https://mirror.otoroshi.io") // TODO + val allowedHosts = hostData.config.allowedHosts // TODO handle valutaion from UI + val urlHost = Uri(url).authority.host.toString() + val allowed = allowedHosts.isEmpty || allowedHosts.contains("*") + if (allowed) { + val builder = env.Ws + .url(url) + .withMethod((context \ "method").asOpt[String].getOrElse("GET")) + .withHttpHeaders((context \ "headers").asOpt[Map[String, String]].getOrElse(Map.empty).toSeq: _*) + .withRequestTimeout( + Duration( + (context \ "request_timeout").asOpt[Long].getOrElse(30000L), // TODO + TimeUnit.MILLISECONDS + ) + ) + .withFollowRedirects((context \ "follow_redirects").asOpt[Boolean].getOrElse(false)) + .withQueryStringParameters((context \ "query").asOpt[Map[String, String]].getOrElse(Map.empty).toSeq: _*) + val bodyAsBytes = context.select("body_bytes").asOpt[Array[Byte]].map(bytes => ByteString(bytes)) + val bodyBase64 = context.select("body_base64").asOpt[String].map(str => ByteString(str).decodeBase64) + val bodyJson = context.select("body_json").asOpt[JsValue].map(str => ByteString(str.stringify)) + val bodyStr = context + .select("body_str") + .asOpt[String] + .orElse(context.select("body").asOpt[String]) + .map(str => ByteString(str)) + val body: Option[ByteString] = bodyStr.orElse(bodyJson).orElse(bodyBase64).orElse(bodyAsBytes) + val request = body match { + case Some(bytes) => builder.withBody(bytes) + case None => builder + } + val out = Await.result( + request + .execute() + .map { res => + val body = res.bodyAsBytes.encodeBase64.utf8String + val headers = res.headers.view.mapValues(_.head) + Json.obj( + "status" -> res.status, + "headers" -> headers, + "body_base64" -> body + ) + }, + Duration(1, TimeUnit.MINUTES) // TODO + ) + plugin.returnString(returns(0), Json.stringify(out)) + } else { + plugin.returnString( + returns(0), + Json.stringify( + Json.obj( + "status" -> 403, + "headers" -> Json.obj("content-type" -> "text/plain"), + "body_base64" -> ByteString(s"you cannot access host: ${urlHost}").encodeBase64.utf8String + ) + ) + ) + } + } + } + } + + def getFunctions(config: WasmConfig, attrs: Option[TypedMap])(implicit + env: Env, + executionContext: ExecutionContext, + mat: Materializer + ): Seq[HostFunctionWithAuthorization] = { + Seq( + HostFunctionWithAuthorization(proxyHttpCall(config), _.asInstanceOf[WasmConfig].authorizations.httpAccess) + ) + } +} + +object HostFunctions { + + def getFunctions(config: WasmConfig, pluginId: String, attrs: Option[TypedMap])(implicit + env: Env, + executionContext: ExecutionContext + ): Array[HostFunction[_ <: HostUserData]] = { + + implicit val mat = env.materializer + + val httpFunctions: Seq[HostFunctionWithAuthorization] = HttpCall.getFunctions(config, attrs) + + val functions: Seq[HostFunctionWithAuthorization] = httpFunctions + + functions + .collect { + case func if func.authorized(config) => func.function + } + .seffectOn(_.map(_.name).mkString(", ")) + .toArray + } +} diff --git a/app/fr/maif/izanami/wasm/integration.scala b/app/fr/maif/izanami/wasm/integration.scala new file mode 100644 index 000000000..e793b641b --- /dev/null +++ b/app/fr/maif/izanami/wasm/integration.scala @@ -0,0 +1,52 @@ +package fr.maif.izanami.wasm + +import akka.stream.Materializer +import fr.maif.izanami.env.Env +import fr.maif.izanami.utils.syntax.implicits.BetterSyntax +import fr.maif.izanami.wasm.host.scala.HostFunctions +import io.otoroshi.wasm4s.scaladsl.security.TlsConfig +import io.otoroshi.wasm4s.scaladsl.{CacheableWasmScript, WasmConfiguration, WasmIntegrationContext, WasmoSettings} +import org.extism.sdk.{HostFunction, HostUserData} +import play.api.Logger +import play.api.libs.ws.WSRequest + +import java.util.concurrent.Executors +import scala.collection.concurrent.TrieMap +import scala.concurrent.{ExecutionContext, Future} + +class IzanamiWasmIntegrationContext(env: Env) extends WasmIntegrationContext { + + implicit val ec: ExecutionContext = env.executionContext + implicit val ev: Env = env + + val logger: Logger = Logger("izanami-wasm") + val materializer: Materializer = env.materializer + val executionContext: ExecutionContext = env.executionContext + val selfRefreshingPools: Boolean = false + val wasmCacheTtl: Long = env.wasmCacheTtl + val wasmQueueBufferSize: Int = env.wasmQueueBufferSize + val wasmScriptCache: TrieMap[String, CacheableWasmScript] = new TrieMap[String, CacheableWasmScript]() + val wasmExecutor: ExecutionContext = ExecutionContext.fromExecutorService( + Executors.newWorkStealingPool(Math.max(32, (Runtime.getRuntime.availableProcessors * 4) + 1)) + ) + + override def url(path: String, tlsConfig: Option[TlsConfig] = None): WSRequest = { + // TODO: support mtls calls + env.Ws.url(path) + } + + override def wasmoSettings: Future[Option[WasmoSettings]] = env.datastores.configuration.readWasmConfiguration().future + + override def wasmConfig(path: String): Future[Option[WasmConfiguration]] = { + val parts = path.split("/") + val tenant = parts.head + val id = parts.last + env.datastores.features.readScriptConfig(tenant, id) + } + + override def wasmConfigs(): Future[Seq[WasmConfiguration]] = env.datastores.features.readAllLocalScripts() + + override def hostFunctions(config: WasmConfiguration, pluginId: String): Array[HostFunction[_ <: HostUserData]] = { + HostFunctions.getFunctions(config.asInstanceOf[WasmConfig], pluginId, None) + } +} \ No newline at end of file diff --git a/app/fr/maif/izanami/wasm/wasm.scala b/app/fr/maif/izanami/wasm/wasm.scala new file mode 100644 index 000000000..d71a54883 --- /dev/null +++ b/app/fr/maif/izanami/wasm/wasm.scala @@ -0,0 +1,205 @@ +package fr.maif.izanami.wasm + +import fr.maif.izanami.env.Env +import fr.maif.izanami.errors.{IzanamiError, WasmError} +import fr.maif.izanami.models.RequestContext +import fr.maif.izanami.utils.syntax.implicits.BetterJsValue +import io.otoroshi.wasm4s.scaladsl._ +import fr.maif.izanami.models.RequestContext +import fr.maif.izanami.utils.syntax.implicits.BetterJsValue +import io.otoroshi.wasm4s.scaladsl._ +import play.api.libs.json._ + +import scala.concurrent.{ExecutionContext, Future} +import scala.util.{Failure, Success, Try} + +case class WasmAuthorizations( + httpAccess: Boolean = false +) { + def json: JsValue = WasmAuthorizations.format.writes(this) +} + +object WasmAuthorizations { + val format = new Format[WasmAuthorizations] { + override def writes(o: WasmAuthorizations): JsValue = Json.obj( + "httpAccess" -> o.httpAccess + ) + override def reads(json: JsValue): JsResult[WasmAuthorizations] = Try { + WasmAuthorizations( + httpAccess = (json \ "httpAccess").asOpt[Boolean].getOrElse(false) + ) + } match { + case Failure(ex) => JsError(ex.getMessage) + case Success(value) => JsSuccess(value) + } + } +} + +case class WasmScriptAssociatedFeatures(id: String, name: String, project: String) + +case class WasmConfigWithFeatures(wasmConfig: WasmConfig, features: Seq[WasmScriptAssociatedFeatures]) + +object WasmConfigWithFeatures { + implicit val wasmConfigAssociatedFeaturesWrites: Writes[WasmScriptAssociatedFeatures] = { feature => + Json.obj( + "name" -> feature.name, + "project" -> feature.project, + "id" -> feature.id + ) + } + + implicit val wasmConfigWithFeaturesWrites: Writes[WasmConfigWithFeatures] = { wasm => + Json.obj( + "config" -> Json.toJson(wasm.wasmConfig)(WasmConfig.format), + "features" -> wasm.features + ) + } +} + +case class WasmConfig( + name: String, + source: WasmSource = WasmSource(WasmSourceKind.Unknown, "", Json.obj()), + memoryPages: Int = 100, + functionName: Option[String] = None, + config: Map[String, String] = Map.empty, + allowedHosts: Seq[String] = Seq.empty, + allowedPaths: Map[String, String] = Map.empty, + //// + // lifetime: WasmVmLifetime = WasmVmLifetime.Forever, + wasi: Boolean = false, + opa: Boolean = false, + instances: Int = 1, + killOptions: WasmVmKillOptions = WasmVmKillOptions.default, + authorizations: WasmAuthorizations = WasmAuthorizations() +) extends WasmConfiguration { + // still here for compat reason + def json: JsValue = Json.obj( + "name" -> name, + "source" -> source.json, + "memoryPages" -> memoryPages, + "functionName" -> functionName, + "config" -> config, + "allowedHosts" -> allowedHosts, + "allowedPaths" -> allowedPaths, + "wasi" -> wasi, + "opa" -> opa, + // "lifetime" -> lifetime.json, + "authorizations" -> authorizations.json, + "instances" -> instances, + "killOptions" -> killOptions.json + ) +} + +object WasmConfig { + val format = new Format[WasmConfig] { + override def reads(json: JsValue): JsResult[WasmConfig] = Try { + val compilerSource = json.select("compiler_source").asOpt[String] + val rawSource = json.select("raw_source").asOpt[String] + val sourceOpt = json.select("source").asOpt[JsObject] + + json + .select("name") + .asOpt[String] + .map(name => { + val source = if (sourceOpt.isDefined) { + WasmSource.format.reads(sourceOpt.get).get + } else { + compilerSource match { + case Some(source) => WasmSource(WasmSourceKind.Wasmo, source, Json.obj("name" -> name)) + case None => + rawSource match { + case Some(source) if source.startsWith("http://") => WasmSource(WasmSourceKind.Http, source, Json.obj("name" -> name)) + case Some(source) if source.startsWith("https://") => WasmSource(WasmSourceKind.Http, source,Json.obj("name" -> name)) + case Some(source) if source.startsWith("file://") => + WasmSource(WasmSourceKind.File, source.replace("file://", ""), Json.obj("name" -> name)) + case Some(source) if source.startsWith("base64://") => + WasmSource(WasmSourceKind.Base64, source.replace("base64://", ""), Json.obj("name" -> name)) + case Some(source) if source.startsWith("entity://") => + WasmSource(WasmSourceKind.Local, source.replace("entity://", ""), Json.obj("name" -> name)) + case Some(source) if source.startsWith("local://") => + WasmSource(WasmSourceKind.Local, source.replace("local://", ""), Json.obj("name" -> name)) + case Some(source) => WasmSource(WasmSourceKind.Base64, source, Json.obj("name" -> name)) + case _ => WasmSource(WasmSourceKind.Unknown, "", Json.obj("name" -> name)) + } + } + } + + WasmConfig( + name = name, + source = source, + memoryPages = (json \ "memoryPages").asOpt[Int].getOrElse(100), + functionName = (json \ "functionName").asOpt[String].filter(_.nonEmpty), + config = (json \ "config").asOpt[Map[String, String]].getOrElse(Map.empty), + allowedHosts = (json \ "allowedHosts").asOpt[Seq[String]].getOrElse(Seq.empty), + allowedPaths = (json \ "allowedPaths").asOpt[Map[String, String]].getOrElse(Map.empty), + wasi = (json \ "wasi").asOpt[Boolean].getOrElse(true), + opa = (json \ "opa").asOpt[Boolean].getOrElse(false), + authorizations = (json \ "authorizations") + .asOpt[WasmAuthorizations](WasmAuthorizations.format.reads) + .orElse((json \ "accesses").asOpt[WasmAuthorizations](WasmAuthorizations.format.reads)) + .getOrElse { + WasmAuthorizations() + }, + instances = json.select("instances").asOpt[Int].getOrElse(1), + killOptions = json + .select("killOptions") + .asOpt[JsValue] + .flatMap(v => WasmVmKillOptions.format.reads(v).asOpt) + .getOrElse(WasmVmKillOptions.default) + ) + }) + + } match { + case Failure(ex) => JsError(ex.getMessage) + case Success(Some(value)) => JsSuccess(value) + case Success(None) => JsError("Missing wasm configuration name") + } + override def writes(o: WasmConfig): JsValue = o.json + } +} + +object WasmUtils { + def handle(config: WasmConfig, requestContext: RequestContext)(implicit ec: ExecutionContext, env: Env): Future[Either[IzanamiError, Boolean]] = { + val context = (requestContext.wasmJson.as[JsObject] ++ Json.obj( + "id" -> requestContext.user, "context" -> requestContext.data, "executionContext" -> requestContext.context.elements + )).stringify + env.wasmIntegration.withPooledVm(config) { vm => + if (config.opa) { + vm.callOpa("execute", context).map { + case Left(err) => throw new RuntimeException(s"Failed to execute wasm feature : ${err.toString()}") // TODO - fix me + case Right((rawResult, _)) => { + val response = Json.parse(rawResult) + val result = response.asOpt[JsArray].getOrElse(Json.arr()) + (result.value.head \ "result").asOpt[Boolean] + .orElse((result.value.head \ "result").asOpt[String].flatMap(s => s.toBooleanOption)) + .toRight({ + env.logger.error(s"Failed to parse wasm result (OPA), result is $result") + WasmError() + }) + } + } + } else { + vm.callExtismFunction("execute", context).map { + case Left(err) => throw new RuntimeException(s"Failed to execute wasm feature : ${err.toString()}") // TODO - fix me + case Right(rawResult) => { + if (rawResult.startsWith("{")) { + val response = Json.parse(rawResult) + + (response \ "active").asOpt[Boolean] + .orElse((response \ "active").asOpt[String].flatMap(s => s.toBooleanOption)) + .toRight({ + env.logger.error(s"Failed to parse wasm result, result is $response") + WasmError() + }) + } else { + rawResult.toBooleanOption.toRight({ + env.logger.error(s"Failed to parse wasm result, result is $rawResult") + WasmError() + }) + } + } + } + } + } + } +} \ No newline at end of file diff --git a/app/fr/maif/izanami/web/ApiKeyController.scala b/app/fr/maif/izanami/web/ApiKeyController.scala new file mode 100644 index 000000000..8f8a04849 --- /dev/null +++ b/app/fr/maif/izanami/web/ApiKeyController.scala @@ -0,0 +1,108 @@ +package fr.maif.izanami.web + +import fr.maif.izanami.env.Env +import fr.maif.izanami.models.{ApiKey, RightLevels, RightTypes, RightUnit} +import play.api.libs.json.JsError.toJson +import play.api.libs.json.{JsValue, Json} +import play.api.mvc._ + +import scala.concurrent.{ExecutionContext, Future} + +class ApiKeyController( + val controllerComponents: ControllerComponents, + val tenantAuthAction: TenantAuthActionFactory, + val keyAuthAction: KeyAuthActionFactory +)(implicit val env: Env) + extends BaseController { + implicit val ec: ExecutionContext = env.executionContext + + def createApiKey(tenant: String): Action[JsValue] = tenantAuthAction(tenant, RightLevels.Write).async(parse.json) { + implicit request => + ApiKey + .read(request.body, tenant) + .map(key => { + env.datastores.users + .hasRightFor( + tenant, + username = request.user, + rights = key.projects.map(p => RightUnit(name = p, rightType = RightTypes.Project, rightLevel = RightLevels.Write)), + tenantLevel = if(key.admin) Some(RightLevels.Admin) else None + ) + .flatMap(authorized => { + if(!authorized) { + Future.successful(Forbidden(Json.obj("message" -> s"${request.user} does not have right on one or more of these projects : ${key.projects.mkString(",")} or is not tenant admin (if admin key was required)"))) + } else { + env.datastores.apiKeys + .createApiKey( + key + .withNewSecret() + .withNewClientId(), + request.user + ) + .map(eitherKey => { + eitherKey.fold( + err => Results.Status(err.status)(Json.toJson(err)), + key => Created(Json.toJson(key)) + ) + }) + } + }) + }) + .recoverTotal(jsError => Future.successful(BadRequest(toJson(jsError)))) + } + + def updateApiKey(tenant: String, name: String): Action[JsValue] = + keyAuthAction(tenant, name, RightLevels.Write).async(parse.json) { implicit request: UserNameRequest[JsValue] => + ApiKey + .read(request.body, tenant) + .map(key => { + env.datastores.apiKeys + .readApiKey(tenant, name) + .map(maybeKey => + maybeKey + .toRight(NotFound(Json.obj("message" -> s"Key ${name} not found"))) + .map(oldKey => (key.projects.filter(!oldKey.projects.contains(_)), oldKey.admin != key.admin)) + ) + .flatMap(eitherRightChanges => + eitherRightChanges.map { + case (newProjects, false) if newProjects.isEmpty => Future.successful(true) + case (newProjects, adminChanged) => {env.datastores.users + .hasRightFor( + tenant, + username = request.user, + rights = newProjects.map(p => RightUnit(name = p, rightType = RightTypes.Project, rightLevel = RightLevels.Write)), + tenantLevel = if(adminChanged) Some(RightLevels.Admin) else None + )} + } match { + case Left(err) => Future.successful(Left(err)) + case Right(future) => future.map(Right(_)) + } + ).map(eitherAuthorized => eitherAuthorized.filterOrElse(b => b, Forbidden(Json.obj("message" -> s"${request.user} does not have right on key projects")))) + .flatMap(e => e.map(_ => { + env.datastores.apiKeys + .updateApiKey(tenant, name, key) + .map(eitherName => { + eitherName.fold( + err => Results.Status(err.status)(Json.toJson(err)), + _ => NoContent + ) + }) + }) fold(r => Future(r), r => r)) + }) + .recoverTotal(jsError => Future.successful(BadRequest(toJson(jsError)))) + } + + def readApiKey(tenant: String): Action[AnyContent] = tenantAuthAction(tenant, RightLevels.Read).async { + implicit request: UserNameRequest[AnyContent] => + env.datastores.apiKeys + .readApiKeys(tenant, request.user) + .map(keys => Ok(Json.toJson(keys))) + } + + def deleteApiKey(tenant: String, name: String): Action[AnyContent] = keyAuthAction(tenant, name, RightLevels.Admin).async { + implicit request: Request[AnyContent] => + env.datastores.apiKeys + .deleteApiKey(tenant, name) + .map(either => either.fold(err => Results.Status(err.status)(Json.toJson(err)), key => NoContent)) + } +} diff --git a/app/fr/maif/izanami/web/AuthAction.scala b/app/fr/maif/izanami/web/AuthAction.scala new file mode 100644 index 000000000..18346c933 --- /dev/null +++ b/app/fr/maif/izanami/web/AuthAction.scala @@ -0,0 +1,288 @@ +package fr.maif.izanami.web + +import fr.maif.izanami.env.Env +import fr.maif.izanami.errors.UserNotFound +import fr.maif.izanami.models.RightLevels.RightLevel +import fr.maif.izanami.models.{ApiKey, ApiKeyWithCompleteRights, UserWithCompleteRightForOneTenant, UserWithRights, UserWithTenantRights} +import fr.maif.izanami.security.JwtService.decodeJWT +import fr.maif.izanami.utils.syntax.implicits.BetterSyntax +import fr.maif.izanami.web.AuthAction.extractClaims +import pdi.jwt.JwtClaim +import play.api.libs.json.Json +import play.api.mvc.Results.{Forbidden, Unauthorized} +import play.api.mvc._ + +import javax.crypto.spec.SecretKeySpec +import scala.concurrent.{ExecutionContext, Future} + + case class ClientKeyRequest[A](request: Request[A], key: ApiKeyWithCompleteRights) extends WrappedRequest[A](request) +case class UserRequestWithCompleteRights[A](request: Request[A], user: UserWithRights) + extends WrappedRequest[A](request) +case class UserRequestWithTenantRights[A](request: Request[A], user: UserWithTenantRights) + extends WrappedRequest[A](request) +case class UserRequestWithCompleteRightForOneTenant[A](request: Request[A], user: UserWithCompleteRightForOneTenant) + extends WrappedRequest[A](request) +case class UserNameRequest[A](request: Request[A], user: String) extends WrappedRequest[A](request) +case class SessionIdRequest[A](request: Request[A], sessionId: String) extends WrappedRequest[A](request) + + +class ClientApiKeyAction(bodyParser: BodyParser[AnyContent], env: Env)(implicit ec: ExecutionContext) + extends ActionBuilder[ClientKeyRequest, AnyContent] { + override def parser: BodyParser[AnyContent] = bodyParser + override protected def executionContext: ExecutionContext = ec + + override def invokeBlock[A](request: Request[A], block: ClientKeyRequest[A] => Future[Result]): Future[Result] = { + val maybeFutureKey = + for ( + clientId <- request.headers.get("Izanami-Client-Id"); + clientSecret <- request.headers.get("Izanami-Client-Secret") + ) yield env.datastores.apiKeys.readAndCheckApiKey(clientId, clientSecret) + + maybeFutureKey + .map(futureKey => + futureKey.flatMap(eitherKey => { + eitherKey.fold(error => Future.successful(Unauthorized(Json.obj("message" -> "Invalid key"))), key => block(ClientKeyRequest(request, key))) + }) + ) + .getOrElse(Future.successful(Unauthorized(Json.obj("message" -> "Invalid key")))) + } +} + +class TenantRightsAction(bodyParser: BodyParser[AnyContent], env: Env)(implicit + ec: ExecutionContext +) extends ActionBuilder[UserRequestWithTenantRights, AnyContent] { + override def parser: BodyParser[AnyContent] = bodyParser + override protected def executionContext: ExecutionContext = ec + + override def invokeBlock[A]( + request: Request[A], + block: UserRequestWithTenantRights[A] => Future[Result] + ): Future[Result] = { + extractClaims(request, env.configuration.get[String]("app.authentication.secret"), env.encryptionKey) + .flatMap(claims => claims.subject) + .fold(Future.successful(Unauthorized("")))(subject => { + env.datastores.users + .findSessionWithTenantRights(subject) + .flatMap { + case None => Unauthorized(Json.obj("message" -> "User is not connected")).future + case Some(user) => block(UserRequestWithTenantRights(request, user)) + } + }) + } +} + +class DetailledAuthAction(bodyParser: BodyParser[AnyContent], env: Env)(implicit + ec: ExecutionContext +) extends ActionBuilder[UserRequestWithCompleteRights, AnyContent] { + override def parser: BodyParser[AnyContent] = bodyParser + override protected def executionContext: ExecutionContext = ec + + override def invokeBlock[A]( + request: Request[A], + block: UserRequestWithCompleteRights[A] => Future[Result] + ): Future[Result] = { + extractClaims(request, env.configuration.get[String]("app.authentication.secret"), env.encryptionKey) + .flatMap(claims => claims.subject) + .fold(Future.successful(Unauthorized(Json.obj("message" -> "Invalid token"))))(subject => { + env.datastores.users + .findSessionWithCompleteRights(subject) + .flatMap { + case None => UserNotFound(subject).toHttpResponse.future + case Some(user) => block(UserRequestWithCompleteRights(request, user)) + } + }) + } +} + +class AdminAuthAction(bodyParser: BodyParser[AnyContent], env: Env)(implicit ec: ExecutionContext) + extends ActionBuilder[UserNameRequest, AnyContent] { + + override def parser: BodyParser[AnyContent] = bodyParser + override protected def executionContext: ExecutionContext = ec + + override def invokeBlock[A](request: Request[A], block: UserNameRequest[A] => Future[Result]): Future[Result] = { + extractClaims(request, env.configuration.get[String]("app.authentication.secret"), env.encryptionKey) + .flatMap(claims => claims.subject) + .fold(Future.successful(Unauthorized(Json.obj("message" -> "Invalid token"))))(subject => { + env.datastores.users + .findAdminSession(subject) + .flatMap { + case Some(username) => block(UserNameRequest(request = request, user = username)) + case None => Future.successful(Forbidden(Json.obj("message" -> "User is not connected"))) + } + }) + } +} + +class AuthenticatedAction(bodyParser: BodyParser[AnyContent], env: Env)(implicit ec: ExecutionContext) + extends ActionBuilder[UserNameRequest, AnyContent] { + + override def parser: BodyParser[AnyContent] = bodyParser + override protected def executionContext: ExecutionContext = ec + + override def invokeBlock[A](request: Request[A], block: UserNameRequest[A] => Future[Result]): Future[Result] = { + extractClaims(request, env.configuration.get[String]("app.authentication.secret"), env.encryptionKey) + .flatMap(claims => claims.subject) + .fold(Future.successful(Unauthorized(Json.obj("message" -> "Invalid token"))))(sessionId => + env.datastores.users.findSession(sessionId).flatMap { + case Some(username) => block(UserNameRequest(request = request, user = username)) + case None => Future.successful(Unauthorized(Json.obj("message" -> "User is not connected"))) + } + ) + } +} + +class AuthenticatedSessionAction(bodyParser: BodyParser[AnyContent], env: Env)(implicit ec: ExecutionContext) + extends ActionBuilder[SessionIdRequest, AnyContent] { + + override def parser: BodyParser[AnyContent] = bodyParser + override protected def executionContext: ExecutionContext = ec + + override def invokeBlock[A](request: Request[A], block: SessionIdRequest[A] => Future[Result]): Future[Result] = { + extractClaims(request, env.configuration.get[String]("app.authentication.secret"), env.encryptionKey) + .flatMap(claims => claims.subject) + .fold(Future.successful(Unauthorized(Json.obj("message" -> "Invalid token"))))(sessionId => + block(SessionIdRequest(request = request, sessionId = sessionId)) + ) + } +} + +class DetailledRightForTenantAction(bodyParser: BodyParser[AnyContent], env: Env, tenant: String)(implicit + ec: ExecutionContext +) extends ActionBuilder[UserRequestWithCompleteRightForOneTenant, AnyContent] { + + override def parser: BodyParser[AnyContent] = bodyParser + override protected def executionContext: ExecutionContext = ec + + override def invokeBlock[A]( + request: Request[A], + block: UserRequestWithCompleteRightForOneTenant[A] => Future[Result] + ): Future[Result] = { + extractClaims(request, env.configuration.get[String]("app.authentication.secret"), env.encryptionKey) + .flatMap(claims => claims.subject) + .fold(Future.successful(Unauthorized(Json.obj("message" -> "Invalid token"))))(subject => { + env.datastores.users + .findSessionWithRightForTenant(subject, tenant) + .flatMap { + case Left(err) => err.toHttpResponse.toFuture + case Right(user) => block(UserRequestWithCompleteRightForOneTenant(request = request, user = user)) + } + }) + } +} + +class TenantAuthAction(bodyParser: BodyParser[AnyContent], env: Env, tenant: String, minimumLevel: RightLevel)(implicit + ec: ExecutionContext +) extends ActionBuilder[UserNameRequest, AnyContent] { + + override def parser: BodyParser[AnyContent] = bodyParser + override protected def executionContext: ExecutionContext = ec + + override def invokeBlock[A](request: Request[A], block: UserNameRequest[A] => Future[Result]): Future[Result] = { + extractClaims(request, env.configuration.get[String]("app.authentication.secret"), env.encryptionKey) + .flatMap(claims => claims.subject) + .fold(Future.successful(Unauthorized(Json.obj("message" -> "Invalid token"))))(subject => { + env.datastores.users + .hasRightForTenant(subject, tenant, minimumLevel) + .flatMap { + case Some(username) => block(UserNameRequest(request = request, user = username)) + case None => Future.successful(Forbidden(Json.obj("message" -> "User does not have enough rights for this operation"))) + } + }) + } +} + +class ProjectAuthAction( + bodyParser: BodyParser[AnyContent], + env: Env, + tenant: String, + project: String, + minimumLevel: RightLevel +)(implicit ec: ExecutionContext) + extends ActionBuilder[Request, AnyContent] { + + override def parser: BodyParser[AnyContent] = bodyParser + override protected def executionContext: ExecutionContext = ec + + override def invokeBlock[A](request: Request[A], block: Request[A] => Future[Result]): Future[Result] = { + extractClaims(request, env.configuration.get[String]("app.authentication.secret"), env.encryptionKey) + .flatMap(claims => claims.subject) + .fold(Future.successful(Unauthorized(Json.obj("message" -> "Invalid token"))))(subject => { + env.datastores.users + .hasRightForProject(subject, tenant, project, minimumLevel) + .flatMap(authorized => + authorized.fold( + err => Future.successful(Results.Status(err.status)(Json.toJson(err))), + authorized => { + if (authorized) { + block(request) + } else { + Future.successful(Forbidden(Json.obj("message" -> "User does not have enough rights for this operation"))) + } + } + ) + ) + }) + } +} + +class KeyAuthAction( + bodyParser: BodyParser[AnyContent], + env: Env, + tenant: String, + key: String, + minimumLevel: RightLevel +)(implicit ec: ExecutionContext) + extends ActionBuilder[UserNameRequest, AnyContent] { + + override def parser: BodyParser[AnyContent] = bodyParser + override protected def executionContext: ExecutionContext = ec + + override def invokeBlock[A](request: Request[A], block: UserNameRequest[A] => Future[Result]): Future[Result] = { + extractClaims(request, env.configuration.get[String]("app.authentication.secret"), env.encryptionKey) + .flatMap(claims => claims.subject) + .fold(Future.successful(Unauthorized(Json.obj("message" -> "Invalid token"))))(subject => { + env.datastores.users + .hasRightForKey(subject, tenant, key, minimumLevel) + .flatMap(authorized => + authorized.fold( + err => Future.successful(Results.Status(err.status)(Json.toJson(err))), + { + case Some(username) => block(UserNameRequest(request = request, user = username)) + case None => Future.successful(Forbidden(Json.obj("message" -> "User does not have enough rights for this operation"))) + } + ) + ) + }) + } +} + +class DetailledRightForTenantFactory(bodyParser: BodyParser[AnyContent], env: Env)(implicit ec: ExecutionContext) { + def apply(tenant: String): DetailledRightForTenantAction = + new DetailledRightForTenantAction(bodyParser, env, tenant) +} + +class KeyAuthActionFactory(bodyParser: BodyParser[AnyContent], env: Env)(implicit ec: ExecutionContext) { + def apply(tenant: String, key: String, minimumLevel: RightLevel): KeyAuthAction = + new KeyAuthAction(bodyParser, env, tenant, key, minimumLevel) +} + +class ProjectAuthActionFactory(bodyParser: BodyParser[AnyContent], env: Env)(implicit ec: ExecutionContext) { + def apply(tenant: String, project: String, minimumLevel: RightLevel): ProjectAuthAction = + new ProjectAuthAction(bodyParser, env, tenant, project, minimumLevel) +} + +class TenantAuthActionFactory(bodyParser: BodyParser[AnyContent], env: Env)(implicit ec: ExecutionContext) { + def apply(tenant: String, minimumLevel: RightLevel): TenantAuthAction = + new TenantAuthAction(bodyParser, env, tenant, minimumLevel) +} + +object AuthAction { + def extractClaims[A](request: Request[A], secret: String, bodySecretKey: SecretKeySpec): Option[JwtClaim] = { + request.cookies + .get("token") + .map(cookie => cookie.value) + .map(token => decodeJWT(token, secret, bodySecretKey)) + .flatMap(maybeClaim => maybeClaim.toOption) + } +} diff --git a/app/fr/maif/izanami/web/ConfigurationController.scala b/app/fr/maif/izanami/web/ConfigurationController.scala new file mode 100644 index 000000000..30c414faa --- /dev/null +++ b/app/fr/maif/izanami/web/ConfigurationController.scala @@ -0,0 +1,97 @@ +package fr.maif.izanami.web + +import fr.maif.izanami.env.Env +import fr.maif.izanami.errors.BadBodyFormat +import fr.maif.izanami.mail.{ConsoleMailProvider, MailGunMailProvider, MailJetMailProvider, MailerTypes, SMTPMailProvider} +import fr.maif.izanami.models.IzanamiConfiguration +import fr.maif.izanami.models.IzanamiConfiguration.{mailGunConfigurationWrite, mailJetConfigurationWrites, mailerReads, SMTPConfigurationWrites} +import fr.maif.izanami.utils.syntax.implicits.BetterSyntax +import play.api.libs.json.{JsError, JsString, JsSuccess, JsValue, Json} +import play.api.mvc.{Action, AnyContent, BaseController, ControllerComponents} + +import scala.concurrent.{ExecutionContext, Future} + +class ConfigurationController( + val controllerComponents: ControllerComponents, + val adminAuthAction: AdminAuthAction +)(implicit val env: Env) + extends BaseController { + implicit val ec: ExecutionContext = env.executionContext; + + + def readStats(): Action[AnyContent] = adminAuthAction.async { implicit request => { + env.datastores.stats.retrieveStats().map(Ok(_)) + } } + def updateConfiguration(): Action[JsValue] = adminAuthAction.async(parse.json) { implicit request => + { + IzanamiConfiguration.configurationReads.reads(request.body) match { + case JsSuccess(configuration, path) => { + env.datastores.configuration.updateConfiguration(configuration).map(_ => NoContent) + } + case JsError(_) => BadBodyFormat().toHttpResponse.future + } + } + } + + def readConfiguration(): Action[AnyContent] = adminAuthAction.async { implicit request: UserNameRequest[AnyContent] => + env.datastores.configuration + .readConfiguration() + .map { + case Left(error) => error.toHttpResponse + case Right(configuration) => Ok(Json.toJson(configuration)) + } + } + + def readExpositionUrl(): Action[AnyContent] = Action { implicit request => + val url = env.configuration.getOptional[String]("app.exposition.backend") + .getOrElse(env.expositionUrl) + Ok(Json.obj("url" -> url)) + } + + def availableIntegrations(): Action[AnyContent] = Action { implicit request => + val isWasmPresent = env.datastores.configuration.readWasmConfiguration().isDefined + val isOidcPresent = env.datastores.configuration.readOIDCConfiguration().isDefined + + Ok(Json.obj( + "wasmo" -> isWasmPresent, + "oidc" -> isOidcPresent + )) + } + + def readMailerConfiguration(id: String): Action[AnyContent] = adminAuthAction.async { + implicit request: UserNameRequest[AnyContent] => + mailerReads.reads(JsString(id)).fold(_ => { + Future.successful(BadRequest(Json.obj("message" -> "Unknown mail provider"))) + }, + mailerType => { + env.datastores.configuration + .readMailerConfiguration(mailerType) + .map { + case Left(error) => error.toHttpResponse + case Right(MailJetMailProvider(configuration)) => Ok(Json.toJson(configuration)) + case Right(ConsoleMailProvider) => Ok(Json.obj()) + case Right(MailGunMailProvider(configuration)) => Ok(Json.toJson(configuration)) + case Right(SMTPMailProvider(configuration)) => Ok(Json.toJson(configuration)) + } + }) + + } + + def updateMailerConfiguration(id: String): Action[JsValue] = adminAuthAction.async(parse.json) { implicit request => + { + mailerReads + .reads(JsString(id)) + .flatMap{ + case MailerTypes.Console => JsError("Can't update built in Console mail provider") + case t@_ => JsSuccess(t) + } + .flatMap(mailer => IzanamiConfiguration.mailProviderConfigurationReads(mailer).reads(request.body)) match { + case JsSuccess(configuration, _) if !configuration.mailerType.toString.equalsIgnoreCase(id) => + BadRequest(Json.obj("message" -> "url id and configuration mailer type should match")).future + case JsSuccess(configuration, _) => + env.datastores.configuration.updateMailerConfiguration(configuration).map(_ => NoContent) + case JsError(_) => BadBodyFormat().toHttpResponse.future + } + } + } +} diff --git a/app/fr/maif/izanami/web/EventController.scala b/app/fr/maif/izanami/web/EventController.scala new file mode 100644 index 000000000..14c06a2d5 --- /dev/null +++ b/app/fr/maif/izanami/web/EventController.scala @@ -0,0 +1,137 @@ +package fr.maif.izanami.web + +import akka.NotUsed +import akka.stream.Materializer +import akka.stream.scaladsl.{BroadcastHub, Flow, Keep, Source} +import fr.maif.izanami.datastores.{FeatureCreated, FeatureDeleted, FeatureUpdated} +import fr.maif.izanami.env.Env +import fr.maif.izanami.errors.FailedToReadEvent +import fr.maif.izanami.models.Feature +import fr.maif.izanami.v1.V1FeatureEvents.{createEvent, deleteEvent, keepAliveEvent, updateEvent} +import io.vertx.pgclient.pubsub.PgSubscriber +import play.api.http.ContentTypes +import play.api.libs.EventSource +import play.api.libs.EventSource.{EventDataExtractor, EventIdExtractor, EventNameExtractor} +import play.api.libs.json.{JsObject, Json} +import play.api.mvc.{Action, AnyContent, BaseController, ControllerComponents} + +import scala.concurrent.duration.DurationInt +import scala.concurrent.{ExecutionContext, Future} +import scala.util.{Failure, Success} + +class EventController(val controllerComponents: ControllerComponents, val clientKeyAction: ClientApiKeyAction)(implicit + val env: Env +) extends BaseController { + implicit val ec: ExecutionContext = env.executionContext; + implicit val materializer: Materializer = env.materializer + val sources:scala.collection.mutable.Map[String, Source[InternalEvent, NotUsed]] = scala.collection.mutable.Map() + + val logger = env.logger + // FIXME create dedicated object instead + private implicit val nameExtractor: EventNameExtractor[JsObject] = + EventNameExtractor[JsObject](_ => None) //Some(event.`type`)) + private implicit val idExtractor: EventIdExtractor[JsObject] = EventIdExtractor[JsObject](event => { + (event \ "_id").asOpt[Long].map(_.toString) + }) //Some(event.key.key)) + private implicit val dataExtractor: EventDataExtractor[JsObject] = + EventDataExtractor[JsObject](event => Json.stringify(event)) + + case class InternalEvent(id: String, project: String, payload: JsObject) + + private def eventSource(tenant: String): Source[InternalEvent, NotUsed] = { + lazy val (queue, source) = Source + .queue[InternalEvent](bufferSize = 1024) + .toMat(BroadcastHub.sink(bufferSize = 1024))(Keep.both) + .run() + + + lazy val subscriber = PgSubscriber.subscriber(env.postgresql.vertx, env.postgresql.connectOptions) + subscriber + .connect() + .onComplete(ar => { + if (ar.succeeded()) { + subscriber + .channel(s"${tenant}-features") + .handler(payload => { + val json = Json.parse(payload); + + val maybeFuturEvent = + for ( + id <- (json \ "id").asOpt[String]; + project <- (json \ "project").asOpt[String]; + eventType <- (json \ "type").asOpt[String] + ) + yield { + val futureEitherEvent = if (eventType.equalsIgnoreCase(FeatureUpdated.toString) || eventType.equalsIgnoreCase(FeatureCreated.toString)) { + env.datastores.features.findById(tenant, id).map { + case Left(value) => Left(value) + case Right(Some(feature)) => Feature.writeFeatureInLegacyFormat(feature) match { + case Some(json) if eventType.equalsIgnoreCase(FeatureUpdated.toString) => Right(Some(updateEvent(id, json))) + case Some(json) if eventType.equalsIgnoreCase(FeatureCreated.toString) => Right(Some(createEvent(id, json))) + case _ => Right(None) + } + case Right(None) => Right(Some(deleteEvent(id))) // Feature has been deleted since event was emitted + } + } else if (eventType.equalsIgnoreCase(FeatureDeleted.toString)) { + Future.successful(Right(Some(deleteEvent(id)))) + } else { + Future + .successful(Left(FailedToReadEvent(payload))) + } + + futureEitherEvent.map { + case Right(Some(json)) => Right(Some(InternalEvent(id=id, project=project, payload=json))) + case Right(None) => Right(None) + case Left(err) => Left(err) + } + } + maybeFuturEvent.getOrElse(Future.successful(Left(FailedToReadEvent(payload)))).map { + case Left(error) => env.logger.error(s"Failed to process event : ${error.message}") + case Right(value) => value.foreach(value => queue.offer((value))) + } + }) + } else { + logger.error("Failed to connect postgres suscriber", ar.cause()) + } + }) + // TODO close subscriber when source terminate + //subscriber.close().scala.foreach(v => logger.debug("Postgres suscriber closed")) + source + } + + val keepAlive: Flow[JsObject, JsObject, NotUsed] = Flow[JsObject].keepAlive( + 30.seconds, + () => { + keepAliveEvent() + } + ) + + def events(pattern: String, domains: String): Action[AnyContent] = clientKeyAction.async { request => + val key = request.key + + val regexpPattern = pattern.replaceAll("\\*", ".*") + val tenant = key.tenant + + val source = sources.getOrElseUpdate(tenant, eventSource(tenant)) + + val resultSource = source.filter{case InternalEvent(id, project, _) => + id.matches(regexpPattern) && (key.admin || key.projects.exists(ap => ap.name == project)) + }.map(_.payload) + + val s = resultSource via keepAlive via EventSource.flow + Future.successful( + Ok.chunked( + s.watchTermination()((_, future) => + future.onComplete { + case Failure(exception) => logger.error("Event source failed", exception) + case Success(_) => { + logger.debug("Event source closed, closing postgres subscriber") + // TODO how to close parent source ? + } + } + ) + ).as(ContentTypes.EVENT_STREAM) + ) + } + +} diff --git a/app/fr/maif/izanami/web/FeatureContextController.scala b/app/fr/maif/izanami/web/FeatureContextController.scala new file mode 100644 index 000000000..99881af42 --- /dev/null +++ b/app/fr/maif/izanami/web/FeatureContextController.scala @@ -0,0 +1,125 @@ +package fr.maif.izanami.web + +import fr.maif.izanami.env.Env +import fr.maif.izanami.errors.BadBodyFormat +import fr.maif.izanami.models.{FeatureContext, RightLevels} +import fr.maif.izanami.utils.syntax.implicits.BetterSyntax +import play.api.libs.json.{JsError, JsSuccess, JsValue, Json} +import play.api.mvc._ + +import scala.concurrent.{ExecutionContext, Future} + +class FeatureContextController( + val controllerComponents: ControllerComponents, + val authAction: ProjectAuthActionFactory, + val tenantAuthAction: TenantAuthActionFactory + )(implicit val env: Env) extends BaseController { + implicit val ec: ExecutionContext = env.executionContext + def createFeatureContext(tenant: String, project: String): Action[JsValue] = createSubContext(tenant, project, FeatureContextPath(Seq())) + + def createSubContext(tenant: String, project: String, parents: FeatureContextPath): Action[JsValue] = authAction(tenant, project, RightLevels.Write).async(parse.json) { + implicit request => + FeatureContext.readFeatureContext(request.body, global=false) match { + case JsSuccess(value, _) => + env.datastores.featureContext + .createFeatureSubContext(tenant, project, parents.elements, value.name) + .map(either => { + either.fold( + err => Results.Status(err.status)(Json.toJson(err)), + context => Created(Json.toJson(context)) + ) + }) + case JsError(errors) => BadRequest(Json.obj("message" -> "bad body format")).future + } + } + + def deleteFeatureStrategy(tenant: String, project: String, context: FeatureContextPath, name: String): Action[AnyContent] = authAction(tenant, project, RightLevels.Write).async { + implicit request: Request[AnyContent] => + env.datastores.featureContext.deleteFeatureStrategy(tenant, project, context.elements, name) + .map { + case Left(err) => err.toHttpResponse + case Right(_) => NoContent + } + } + + def deleteFeatureContext(tenant: String, project: String, context: FeatureContextPath): Action[AnyContent] = authAction(tenant, project, RightLevels.Write).async { + implicit request: Request[AnyContent] => + env.datastores.featureContext.deleteContext(tenant, project, context.elements) + .map { + case Left(err) => err.toHttpResponse + case Right(_) => NoContent + } + } + + def readFeatureContexts(tenant: String, project: String): Action[AnyContent] = authAction(tenant, project, RightLevels.Read).async { + implicit request: Request[AnyContent] => + env.datastores.featureContext + .readFeatureContexts(tenant, project) + .map(createFeatureContextHierarchy) + .map(keys => Ok(Json.toJson(keys))) + } + + def createFeatureContextHierarchy(contexts: Seq[FeatureContext]): Seq[FeatureContext] = { + val byParent = contexts.groupBy(g => g.parent) + val topOfTheTree = byParent.getOrElse(null, Seq()) + + def fillChildren(current: FeatureContext): FeatureContext = { + val children = byParent.getOrElse(current.id, Seq()) + if (children.isEmpty) + current + else + current.copy(children = children.map(g => fillChildren(g))) + } + + topOfTheTree.map(fillChildren) + } + + def createFeatureStrategy(tenant: String, project: String, parents: FeatureContextPath, name: String): Action[JsValue] = + authAction(tenant, project, RightLevels.Write).async(parse.json) { implicit request => + FeatureContext.readcontextualFeatureStrategyRead(request.body, name) match { + case JsSuccess(value, path) => + env.datastores.featureContext + .updateFeatureStrategy(tenant, project, parents.elements, name, value) + .map(eitherCreated => { + eitherCreated.fold( + err => Results.Status(err.status)(Json.toJson(err)), + _ => NoContent + ) + }) + case JsError(errors) => BadRequest(Json.obj("message" -> "bad body format")).future + } + } + + def createGlobalSubContext(tenant: String, parents: FeatureContextPath=FeatureContextPath()): Action[JsValue] = tenantAuthAction(tenant, RightLevels.Write).async(parse.json) { + implicit request => + FeatureContext.readFeatureContext(request.body, global=true) match { + case JsSuccess(ctx, _) => env.datastores.featureContext.createGlobalFeatureContext(tenant, parents.elements, ctx) map { + case Left(err) => err.toHttpResponse + case Right(result) => Created(Json.toJson(result)) + } + case JsError(error) => BadBodyFormat().toHttpResponse.future + } + } + + def createGlobalRootSubContext(tenant: String): Action[JsValue] = createGlobalSubContext(tenant, FeatureContextPath()) + + def readGlobalContexts(tenant: String, all: Boolean): Action[AnyContent] = tenantAuthAction(tenant, RightLevels.Read).async { + implicit request => { + if(all) { + env.datastores.featureContext.readAllLocalFeatureContexts(tenant).map(createFeatureContextHierarchy).map(ctx => Ok(Json.toJson(ctx))) + } else { + env.datastores.featureContext.readGlobalFeatureContexts(tenant).map(createFeatureContextHierarchy).map(ctx => Ok(Json.toJson(ctx))) + } + } + } + + def deleteGlobalFeatureContext(tenant: String, context: fr.maif.izanami.web.FeatureContextPath): Action[AnyContent] = tenantAuthAction(tenant, RightLevels.Write).async { + implicit request => + env.datastores.featureContext.deleteGlobalFeatureContext(tenant, context.elements).map { + case Left(err) => err.toHttpResponse + case Right(_) => NoContent + } + } +} + + diff --git a/app/fr/maif/izanami/web/FeatureContextPath.scala b/app/fr/maif/izanami/web/FeatureContextPath.scala new file mode 100644 index 000000000..8967506e8 --- /dev/null +++ b/app/fr/maif/izanami/web/FeatureContextPath.scala @@ -0,0 +1,39 @@ +package fr.maif.izanami.web + +import play.api.mvc.{PathBindable, QueryStringBindable} + +case class FeatureContextPath(elements: Seq[String]=Seq()) + + +object FeatureContextPath { + implicit def pathBinder(implicit strBinder: PathBindable[String]): PathBindable[FeatureContextPath] = new PathBindable[FeatureContextPath] { + override def bind(key: String, value: String): Either[String, FeatureContextPath] = { + strBinder.bind(key, value).map(str => { + FeatureContextPath(str.split("/").toSeq) + }) + } + override def unbind(key: String, path: FeatureContextPath): String = { + path.elements.mkString("/") + } + } + + implicit def queryStringBindable(implicit + seqBinder: QueryStringBindable[Seq[String]] + ): QueryStringBindable[FeatureContextPath] = + new QueryStringBindable[FeatureContextPath] { + override def bind(key: String, params: Map[String, Seq[String]]): Option[Either[String, FeatureContextPath]] = { + for ( + eitherContext <- seqBinder.bind("context", params) + ) yield { + Right( + FeatureContextPath(elements=eitherContext.map(seq => seq.filter(str => str.nonEmpty).flatMap(str => str.split("/"))).getOrElse(Seq())) + ) + } + } + + override def unbind(key: String, request: FeatureContextPath): String = { + throw new NotImplementedError("") + } + } +} + diff --git a/app/fr/maif/izanami/web/FeatureController.scala b/app/fr/maif/izanami/web/FeatureController.scala new file mode 100644 index 000000000..f58f447a7 --- /dev/null +++ b/app/fr/maif/izanami/web/FeatureController.scala @@ -0,0 +1,545 @@ +package fr.maif.izanami.web + +import fr.maif.izanami.env.Env +import fr.maif.izanami.errors.{FeatureNotFound, IzanamiError, TagDoesNotExists} +import fr.maif.izanami.models.Feature._ +import fr.maif.izanami.models._ +import fr.maif.izanami.utils.syntax.implicits.BetterSyntax +import fr.maif.izanami.v1.OldFeature +import io.otoroshi.wasm4s.scaladsl.WasmSourceKind +import play.api.libs.Files +import play.api.libs.json.Format.GenericFormat +import play.api.libs.json._ +import play.api.mvc._ + +import java.time.{Instant, ZoneId} +import java.util.{Base64, TimeZone} +import scala.concurrent.{ExecutionContext, Future} +import scala.io.Source +import scala.util.{Failure, Success, Try} + +class FeatureController( + val env: Env, + val controllerComponents: ControllerComponents, + val projectAuthAction: ProjectAuthActionFactory, + val authenticatedAction: AuthenticatedAction, + val detailledRightForTenanFactory: DetailledRightForTenantFactory +) extends BaseController { + implicit val ec: ExecutionContext = env.executionContext + + def testFeature(tenant: String, user: String, date: Instant): Action[JsValue] = + authenticatedAction.async(parse.json) { implicit request => + { + Feature.readFeature((request.body.as[JsObject]) + ("name" -> Json.toJson("test")), "") match { + case JsError(e) => BadRequest(Json.obj("message" -> "bad body format")).future + case JsSuccess(feature, _) => { + val featureToEval = feature match { + case w: WasmFeature if w.wasmConfig.source.kind == WasmSourceKind.Local => + w.copy(wasmConfig = + w.wasmConfig.copy(source = w.wasmConfig.source.copy(path = s"${tenant}/${w.wasmConfig.source.path}")) + ) + case f => f + } + Feature + .writeFeatureForCheck(featureToEval, RequestContext(tenant = "_test_", user = user, now = date), env) + .map { + case Left(value) => value.toHttpResponse + case Right(json) => Ok(json) + } + } + + } + } + } + + def testExistingFeatureWithoutContext(tenant: String, id: String, user: String, date: Instant): Action[AnyContent] = + testExistingFeature(tenant, FeatureContextPath(Seq()), id, user, date) + + def testExistingFeature( + tenant: String, + context: FeatureContextPath, + id: String, + user: String, + date: Instant + ): Action[AnyContent] = authenticatedAction.async { implicit request => + { + env.datastores.features + .findById( + tenant, + id + ) + .flatMap(eitherFeature => { + eitherFeature.fold( + err => Future(Results.Status(err.status)(Json.toJson(err))), + maybeFeature => { + maybeFeature + .map(feature => + env.datastores.featureContext + .readStrategyForContext(tenant, context.elements, feature) + .flatMap { + case Some(strategy) => { + strategy + .active(RequestContext(tenant = tenant, user, context = context), env) + .map { + case Left(value) => value.toHttpResponse + case Right(active) => + Ok( + Json.obj( + "active" -> active, + "project" -> feature.project, + "name" -> feature.name + ) + ) + } + } + case None => + Feature + .writeFeatureForCheck( + feature, + RequestContext(tenant = tenant, user = user, now = date, context = context), + env + ) + .map { + case Left(error) => error.toHttpResponse + case Right(json) => Ok(json) + } + } + ) + .getOrElse(Future(NotFound(Json.obj("message" -> s"Feature $id does not exist")))) + } + ) + }) + + } + } + + def checkFeatureForContext( + id: String, + user: String, + context: fr.maif.izanami.web.FeatureContextPath + ): Action[AnyContent] = Action.async { implicit request => + { + val maybeBody = request.body.asJson.flatMap(jsValue => jsValue.asOpt[JsObject]) + val basicAuth: Option[(String, String)] = request.headers + .get("Authorization") + .map(header => header.split("Basic ")) + .filter(splitted => splitted.length == 2) + .map(splitted => splitted(1)) + .map(header => { + Base64.getDecoder.decode(header.getBytes) + }) + .map(bytes => new String(bytes)) + .map(header => header.split(":")) + .filter(arr => arr.length == 2) + .map(arr => (arr(0), arr(1))) + val customHeaders: Option[(String, String)] = for { + clientId <- request.headers.get("Izanami-Client-Id") + clientSecret <- request.headers.get("Izanami-Client-Secret") + } yield (clientId, clientSecret) + val authTuple: Option[(String, String)] = basicAuth.orElse(customHeaders) + + authTuple match { + case Some((clientId, clientSecret)) => { + val futureTenant = ApiKey.extractTenant(clientId) match { + case None => env.datastores.apiKeys.findLegacyKeyTenant(clientId) + case s @ Some(_) => s.future + } + + futureTenant + .flatMap { + case Some(tenant) => + env.datastores.features + .findByIdForKey(tenant, id, context.elements, clientId, clientSecret) + .map(maybeFeature => maybeFeature.map(feature => (tenant, feature))) + case None => Future.successful(None) + } + .flatMap { + case Some((tenant, feature)) => + Feature + .writeFeatureForCheck( + feature, + RequestContext(tenant = tenant, user = user, context = context, data=maybeBody.getOrElse(Json.obj())), + env = env, + ) + .map { + case Left(error) => error.toHttpResponse + case Right(json) => Ok(json) + } + case None => Unauthorized(Json.obj("message" -> "Key does not authorize read for this feature")).future + } + } + case None => Unauthorized(Json.obj("message" -> "Missing or incorrect authorization headers")).future + } + } + } + + def processInputSeqString(input: Seq[String]): Set[String] = { + input.filter(str => str.nonEmpty).flatMap(str => str.split(",")).toSet + } + + def searchFeatures(tenant: String, tag: String): Action[AnyContent] = detailledRightForTenanFactory(tenant).async { + implicit request => + env.datastores.features + .searchFeature(tenant, if (tag.isBlank) Set() else Set(tag)) + .map(features => Ok(Json.toJson(features)(Writes.seq(featureWrite)))) + } + + def evaluateFeaturesForContext( + user: String, + conditions: Boolean, + date: Option[Instant], + featureRequest: FeatureRequest + ): Action[AnyContent] = Action.async { implicit request => + { + val maybeBody = request.body.asJson.flatMap(jsValue => jsValue.asOpt[JsObject]) + val basicAuth: Option[(String, String)] = request.headers + .get("Authorization") + .map(header => header.split("Basic ")) + .filter(splitted => splitted.length == 2) + .map(splitted => splitted(1)) + .map(header => { + Base64.getDecoder.decode(header.getBytes) + }) + .map(bytes => new String(bytes)) + .map(header => header.split(":")) + .filter(arr => arr.length == 2) + .map(arr => (arr(0), arr(1))) + val customHeaders: Option[(String, String)] = for { + clientId <- request.headers.get("Izanami-Client-Id") + clientSecret <- request.headers.get("Izanami-Client-Secret") + } yield (clientId, clientSecret) + val authTuple: Option[(String, String)] = basicAuth.orElse(customHeaders) + + authTuple match { + case None => Unauthorized(Json.obj("message" -> "Missing or incorrect authorization headers")).future + case Some((clientId, clientSecret)) => { + val futureMaybeTenant = ApiKey + .extractTenant(clientId) + .map(t => Future.successful(Some(t))) + .getOrElse(env.datastores.apiKeys.findLegacyKeyTenant(clientId)) + + futureMaybeTenant.flatMap { + case None => Forbidden(Json.obj("message" -> "Key is not authorized for this tenant")).future + case Some(tenant) => { + if (conditions) { + val futureFeaturesByProject = env.datastores.features.doFindByRequestForKey(tenant, featureRequest, clientId, clientSecret, true) + futureFeaturesByProject.transformWith { + case Failure(exception) => InternalServerError("").future + case Success(Left(error)) => error.toHttpResponse.future + case Success(Right(featuresByProjects)) if featuresByProjects.isEmpty => Forbidden(Json.obj("message" -> "Incorrect ClientId / ClientSecret tuple")).future + case Success(Right(featuresByProjects)) => { + val strategiesByFeatureId = featuresByProjects.toSeq.flatMap { + case (projectId, features) => { + val futures:Seq[Future[Either[(String, IzanamiError), (String, JsObject)]]] = features.toSeq.map { case (featureId, featureAndContexts) => { + val strategyByCtx = featureAndContexts.map { + case (Some(ctx), feat) => (ctx, feat) + case (None, feat) => ("", feat) + }.toMap + + // TODO fatorize this separator + val ctxStr = featureRequest.context.mkString("_") + val strategyToUse = if (ctxStr.isBlank) { + strategyByCtx("") + } else { + strategyByCtx.filter { case (ctx, f) => ctxStr.startsWith(ctx) } + .toSeq.sortWith { + case ((c1, _), (c2, _)) if c1.length < c2.length => false + case _ => true + }.headOption.map(_._2).getOrElse(strategyByCtx("")) + } + + val jsonStrategies = Json.toJson(strategyByCtx.map { case (ctx, feature) => { + (ctx.replace("_", "/"), (feature match { + case w: WasmFeature => Feature.featureWrite.writes(w).as[JsObject] - "wasmConfig" - "tags" - "name" - "description" - "id" - "project" ++ Json.obj("wasmConfig" -> Json.obj("name" -> w.wasmConfig.name)) + case lf: SingleConditionFeature => Feature.featureWrite.writes(lf.toModernFeature).as[JsObject] - "tags" - "name" - "description" - "id" - "project" + case f => Feature.featureWrite.writes(f).as[JsObject] + }) - "metadata" - "tags" - "name" - "description" - "id" - "project") + } + }).as[JsObject] + + writeFeatureForCheck(strategyToUse, RequestContext( + tenant = tenant, + user = user, + now = date.getOrElse(Instant.now()), + context = FeatureContextPath(featureRequest.context), + data=maybeBody.getOrElse(Json.obj()) + ), env = env + ).map { + case Left(err) => Left((featureId, err)) + case Right(jsonFeature) => { + val entry = jsonFeature ++ Json.obj("conditions" -> jsonStrategies) + Right((featureId, entry)) + } + } + } + } + futures + } + } + + Future.sequence(strategiesByFeatureId).map(s => { + s.map{ + case Left((featureId, error)) => (featureId, Json.obj("error" -> error.message)) + case Right((featureId, json)) => (featureId, json) + }.toMap + }).map(map => Ok(Json.toJson(map))) + + } + } + } else { + val futureFeaturesByProject = env.datastores.features.findByRequestForKey( + tenant, + featureRequest, + clientId, + clientSecret + ) + + futureFeaturesByProject.transformWith { + case Failure(exception) => InternalServerError("").future + case Success(Left(error)) => error.toHttpResponse.future + case Success(Right(featuresByProjects)) if featuresByProjects.isEmpty => Forbidden(Json.obj("message" -> "Incorrect ClientId / ClientSecret tuple")).future + case Success(Right(featuresByProjects)) => { + Future + .sequence( + featuresByProjects.values.flatten + .map(feature => + Feature + .writeFeatureForCheck( + feature, + context = RequestContext( + tenant = tenant, + user = user, + now = date.getOrElse(Instant.now()), + context = FeatureContextPath(featureRequest.context), + data=maybeBody.getOrElse(Json.obj()) + ), + env = env, + ) + .map(either => (feature.id, either)) + .map { + case (id, Left(error)) => id -> Json.obj("error" -> error.message) + case (id, Right(active)) => id -> active + } + ) + ) + .map(_.toMap) + .map(map => Ok(Json.toJson(map))) + } + } + } + + } + } + + } + } + } + + } + + def testFeaturesForContext( + tenant: String, + user: String, + date: Option[Instant], + featureRequest: FeatureRequest + ): Action[AnyContent] = authenticatedAction.async { implicit request => + val futureFeaturesByProject = + env.datastores.features.findByRequestV2(tenant, featureRequest, contexts = featureRequest.context, request.user) + + futureFeaturesByProject.flatMap(featuresByProjects => { + val resultingFeatures = featuresByProjects.values.flatMap(featSeq => featSeq.map(f => f.id)).toSet + if (!featureRequest.projects.subsetOf(featuresByProjects.keySet)) { + val missing = featureRequest.projects.diff(featuresByProjects.keySet) + Forbidden(Json.obj("message" -> s"You're not allowed for projects ${missing.mkString(",")}")).future + } else if (!featureRequest.features.subsetOf(resultingFeatures)) { + val missing = featureRequest.features.diff(resultingFeatures) + Forbidden( + Json.obj( + "message" -> s"You're not allowed for features ${missing.mkString(",")}, you don't have right for this project" + ) + ).future + } else { + Future + .sequence( + featuresByProjects.values.flatten + .map(feature => + feature + .active( + RequestContext( + tenant = tenant, + user = user, + now = date.getOrElse(Instant.now()), + context = FeatureContextPath(featureRequest.context) + ), + env + ) + .map(either => (feature, either)) + .map { + case (feature, Left(error)) => + feature.id -> Json + .obj("error" -> error.message, "name" -> feature.name, "project" -> feature.project) + case (feature, Right(active)) => + feature.id -> Json.obj("active" -> active, "name" -> feature.name, "project" -> feature.project) + } + ) + ) + .map(_.toMap) + .map(map => Ok(Json.toJson(map))) + } + }) + } + + def patchFeatures(tenant: String): Action[JsValue] = detailledRightForTenanFactory(tenant).async(parse.json) { + implicit request => + request.body + .asOpt[Seq[FeaturePatch]] + .map(fs => { + env.datastores.features + .findFeaturesProjects(tenant, fs.map(fp => fp.id).toSet) + .map(projects => { + projects.foreach(project => request.user.hasRightForProject(project, RightLevels.Write)) + }) + env.datastores.features.applyPatch(tenant, fs).map(_ => NoContent) + }) + .getOrElse(BadRequest("").future) + } + + def createFeature(tenant: String, project: String): Action[JsValue] = + projectAuthAction(tenant, project, RightLevels.Write).async(parse.json) { implicit request => + Feature.readFeature(request.body, project) match { + case JsError(e) => BadRequest(Json.obj("message" -> "bad body format")).future + case JsSuccess(feature, _) => { + env.datastores.tags + .readTags(tenant, feature.tags) + .flatMap { + case tags if tags.size < feature.tags.size => { + val tagsToCreate = feature.tags.diff(tags.map(t => t.name).toSet) + env.datastores.tags.createTags(tagsToCreate.map(name => TagCreationRequest(name = name)).toList, tenant) + } + case tags => Right(tags).toFuture + } + .flatMap(_ => + env.datastores.features + .create(tenant, project, feature) + .flatMap { either => + { + either match { + case Right(id) => + env.datastores.features + .findById(tenant, id) + .map(either => either.flatMap(o => o.toRight(FeatureNotFound(id.toString)))) + case Left(err) => Future.successful(Left(err)) + } + } + } + .map(maybeFeature => + maybeFeature + .fold( + err => + err match { + case e: TagDoesNotExists => Results.Status(BAD_REQUEST)(Json.toJson(err)) + case e => Results.Status(e.status)(Json.toJson(e)) + }, + feat => Created(Json.toJson(feat)(featureWrite)) + ) + ) + ) + } + } + } + + def updateFeature(tenant: String, id: String): Action[JsValue] = + detailledRightForTenanFactory(tenant).async(parse.json) { implicit request => + Feature.readFeature(request.body) match { + case JsError(e) => BadRequest(Json.obj("message" -> "bad body format")).future + case JsSuccess(feature, _) => { + env.datastores.tags + .readTags(tenant, feature.tags) + .flatMap { + case tags if tags.size < feature.tags.size => { + val tagsToCreate = feature.tags.diff(tags.map(t => t.name).toSet) + env.datastores.tags.createTags(tagsToCreate.map(name => TagCreationRequest(name = name)).toList, tenant) + } + case tags => Right(tags).toFuture + } + .flatMap(_ => + env.datastores.features + .findById(tenant, id) + .flatMap { + case Left(err) => err.toHttpResponse.future + case Right(None) => NotFound("").toFuture + case Right(Some(oldFeature)) if !canCreateOrModifyFeature(oldFeature, request.user) => + Forbidden("Your are not allowed to modify this feature").toFuture + case Right(Some(oldFeature)) => { + env.datastores.features + .update(tenant = tenant, id = id, feature = feature) + .flatMap { + case Right(id) => env.datastores.features.findById(tenant, id) + case Left(err) => Future.successful(Left(err)) + } + .map(maybeFeature => + convertReadResult( + maybeFeature, + callback = feature => Ok(Json.toJson(feature)(featureWrite)), + id = id.toString + ) + ) + } + } + ) + } + } + } + + def isKeyAccreditedForFeature(feature: AbstractFeature, apiKey: ApiKeyWithCompleteRights): Boolean = + apiKey.enabled && apiKey.projects.exists(p => p.name == feature.project) + + def convertReadResult( + either: Either[IzanamiError, Option[AbstractFeature]], + callback: AbstractFeature => Result, + id: String = "" + ): Result = { + either + .flatMap(o => o.toRight(FeatureNotFound(id))) + .fold( + err => Results.Status(err.status)(Json.toJson(err)), + feat => callback(feat) + ) + } + + def canCreateOrModifyFeature(feature: AbstractFeature, user: UserWithCompleteRightForOneTenant): Boolean = { + if (user.admin) { + true + } else { + val projectRight = user.tenantRight.flatMap(tr => tr.projects.get(feature.project)) + projectRight.exists(currentRight => + RightLevels.superiorOrEqualLevels(RightLevels.Write).contains(currentRight.level) + ) + } + } + + def deleteFeature(tenant: String, id: String): Action[AnyContent] = detailledRightForTenanFactory(tenant).async { + implicit request => + env.datastores.features + .findById(tenant, id) + .flatMap { + case Left(err) => err.toHttpResponse.future + case Right(None) => NotFound("").toFuture + case Right(Some(feature)) => { + + if (canCreateOrModifyFeature(feature, request.user)) { + env.datastores.features + .delete(tenant, id) + .map(maybeFeature => + maybeFeature + .map(_ => NoContent) + .getOrElse(NotFound("Feature not found")) + ) + } else { + Forbidden("").toFuture + } + } + } + + } +} diff --git a/app/fr/maif/izanami/web/FrontendController.scala b/app/fr/maif/izanami/web/FrontendController.scala new file mode 100644 index 000000000..d15b1d125 --- /dev/null +++ b/app/fr/maif/izanami/web/FrontendController.scala @@ -0,0 +1,29 @@ +package fr.maif.izanami.web + +import controllers.Assets +import fr.maif.izanami.env.Env +import play.api.mvc.{Action, AnyContent, BaseController, ControllerComponents} + +class FrontendController(val assets: Assets, val controllerComponents: ControllerComponents)(implicit val env: Env) extends BaseController { + + def headers = List( + "Access-Control-Allow-Origin" -> "*", + "Access-Control-Allow-Methods" -> "GET, POST, OPTIONS, DELETE, PUT", + "Access-Control-Max-Age" -> "3600", + "Access-Control-Allow-Headers" -> "Origin, Content-Type, Accept, Authorization, Izanami-Client-Id, Izanami-Client-Secret", + "Access-Control-Allow-Credentials" -> "true" + ) + + def rootOptions = options("/") + + def options(url: String) = Action { request => + NoContent.withHeaders(headers: _*) + } + def index: Action[AnyContent] = assets.at("index.html") + + def assetOrDefault(resource: String): Action[AnyContent] = /*if (resource.startsWith("/api")){ + Action.async(r => errorHandler.onClientError(r, NOT_FOUND, "Not found")) + } else {*/ + if (resource.contains(".")) assets.at(resource) else index + //} +} diff --git a/app/fr/maif/izanami/web/ImportController.scala b/app/fr/maif/izanami/web/ImportController.scala new file mode 100644 index 000000000..984043fd8 --- /dev/null +++ b/app/fr/maif/izanami/web/ImportController.scala @@ -0,0 +1,442 @@ +package fr.maif.izanami.web + +import akka.util.ByteString +import fr.maif.izanami.env.Env +import fr.maif.izanami.errors.IzanamiError +import fr.maif.izanami.models.{AbstractFeature, ApiKey, Feature, RightLevels, UserWithRights, WasmFeature} +import fr.maif.izanami.utils.syntax.implicits.BetterSyntax +import fr.maif.izanami.v1.OldKey.{oldKeyReads, toNewKey} +import fr.maif.izanami.v1.OldScripts.doesUseHttp +import fr.maif.izanami.v1.OldUsers.{oldUserReads, toNewUser} +import fr.maif.izanami.v1.{JavaScript, OldFeature, OldGlobalScript, WasmManagerClient} +import fr.maif.izanami.wasm.WasmConfig +import fr.maif.izanami.web.ImportController.{extractProjectAndName, parseStrategy, readFile, scriptIdToNodeCompatibleName, unnest} +import fr.maif.izanami.web.ImportState.importResultWrites +import io.otoroshi.wasm4s.scaladsl.WasmSourceKind.{Base64, Wasmo} +import play.api.libs.Files +import play.api.libs.json._ +import play.api.mvc._ + +import java.net.URI +import java.time.ZoneId +import java.util.UUID +import scala.collection.immutable.Seq +import scala.concurrent.{ExecutionContext, Future} +import scala.io.Source +import scala.util.{Right, Try} + +sealed trait ImportStatus +case object Pending extends ImportStatus { + override def toString: String = this.productPrefix +} +case object Success extends ImportStatus { + override def toString: String = this.productPrefix +} +case object Failed extends ImportStatus { + override def toString: String = this.productPrefix +} + +sealed trait ImportState { + def id: UUID + def status: ImportStatus +} +sealed trait ImportResult extends ImportState +case class ImportSuccess(id: UUID, features: Int, users: Int, scripts: Int, keys: Int, incompatibleScripts: Seq[String] = Seq()) extends ImportResult { + val status = Success +} +case class ImportFailure(id: UUID, errors: Seq[String]) extends ImportResult { + val status = Failed +} +case class ImportPending(id: UUID) extends ImportState { + val status = Pending +} + +object ImportState { + + val importSuccessReads: Reads[ImportSuccess] = json => { + for { + id <- (json \ "id").asOpt[UUID] + features <- (json \ "features").asOpt[Int] + scripts <- (json \ "scripts").asOpt[Int] + keys <- (json \ "keys").asOpt[Int] + users <- (json \ "users").asOpt[Int] + incompatibleScripts <- (json \ "incompatibleScripts").asOpt[Seq[String]] + } yield ImportSuccess(id=id, features=features,scripts=scripts,keys=keys,users=users,incompatibleScripts=incompatibleScripts) + }.map(JsSuccess(_)).getOrElse(JsError("Failed to read import success")) + + val importFailureReads: Reads[ImportFailure] = json => { + for { + id <- (json \ "id").asOpt[UUID] + errors <- (json \ "errors").asOpt[Seq[String]] + } yield ImportFailure(id = id, errors=errors) + }.map(JsSuccess(_)).getOrElse(JsError("Failed to read import failure")) + + val importPendingReads: Reads[ImportPending] = json => { + for { + id <- (json \ "id").asOpt[UUID] + } yield ImportPending(id = id) + }.map(JsSuccess(_)).getOrElse(JsError("Failed to read import pending")) + + val importSuccessWrites: Writes[ImportSuccess] = s => { + Json.obj( + "status" -> s.status.toString, + "id" -> s.id, + "features" -> s.features, + "keys" -> s.keys, + "scripts" -> s.scripts, + "users" -> s.users, + "incompatibleScripts" -> s.incompatibleScripts + ) + } + + val importFailureWrites: Writes[ImportFailure] = s => { + Json.obj( + "status" -> s.status.toString, + "id" -> s.id, + "errors" -> s.errors + ) + } + + val importPendingWrites: Writes[ImportPending] = s => { + Json.obj( + "status" -> s.status.toString, + "id" -> s.id + ) + } + + val importResultWrites: Writes[ImportState] = { + case s@ImportSuccess(id, features, users, scripts, keys, incompatibleScripts) => importSuccessWrites.writes(s) + case f@ImportFailure(id, errors) => importFailureWrites.writes(f) + case p@ImportPending(id) => importPendingWrites.writes(p) + } + + + val importResultReads: Reads[ImportState] = json => { + (json \ "status").asOpt[String].map(_.toUpperCase).map { + case "SUCCESS" => importSuccessReads.reads(json) + case "FAILED" => importFailureReads.reads(json) + case "PENDING" => importPendingReads.reads(json) + case _ => JsError("Can't find known status field") + }.getOrElse(JsError("Can't find known status field")) + } +} + + +class ImportController (val env: Env, + val controllerComponents: ControllerComponents, + val tenantAuthAction: TenantAuthActionFactory, + val wasmManagerClient: WasmManagerClient + ) extends BaseController { + + implicit val ec: ExecutionContext = env.executionContext; + + def deleteImportStatus(tenant: String, id: String): Action[AnyContent] = tenantAuthAction(tenant, RightLevels.Admin).async { + implicit request => { + env.datastores.tenants.deleteImportStatus(UUID.fromString(id)).map(_ => NoContent) + } + } + + def readImportStatus(tenant: String, id: String): Action[AnyContent] = tenantAuthAction(tenant, RightLevels.Admin).async { + implicit request => { + env.datastores.tenants.readImportStatus(UUID.fromString(id)).map { + case Some(importResult) => Ok(Json.toJson(importResult)(importResultWrites)) + case None => NotFound(Json.obj("message" -> "Can't find this import")) + } + } + } + + def importData( + tenant: String, + conflict: String, + timezone: String, + deduceProject: Boolean, + create: Option[Boolean], + project: Option[String], + projectPartSize: Option[Int], + inlineScript: Option[Boolean] + ): Action[MultipartFormData[Files.TemporaryFile]] = + tenantAuthAction(tenant, RightLevels.Admin).async(parse.multipartFormData) { implicit request => + val isBase64 = inlineScript.getOrElse(true) + + + def runImport(id: UUID): Future[ImportResult] = { + val strategy = parseStrategy(conflict) + + val files: Map[String, URI] = request.body.files.map(f => (f.key, f.ref.path.toUri)).toMap + + case class MigrationData( + features: Seq[AbstractFeature] = Seq(), + users: Seq[UserWithRights] = Seq(), + keys: Seq[ApiKey] = Seq(), + scripts: Seq[OldGlobalScript] = Seq(), + excludedScripts: Seq[OldGlobalScript] = Seq() + ) + + + val projectChoiceStrategy: ProjectChoiceStrategy = if (!deduceProject) { + // TODO handle missing project + FixedProject(project.get) + } else { + DeduceProject(projectPartSize.getOrElse(1)) + } + + val maybeEitherScripts = files.get("scripts") + .map(uri => unnest(readFile(uri, OldFeature.globalScriptReads).map(_.map(Right(_))))) + .map(either => either.map(scripts => scripts.map(s => s.copy(id = scriptIdToNodeCompatibleName(s.id))))) + + val maybeEitherFeatures = files + .get("features") + .map(uri => + unnest( + readFile(uri, OldFeature.oldFeatureReads).map(features => { + val globalScriptById: Map[String, OldGlobalScript] = maybeEitherScripts + .flatMap(_.toOption.map(s => s.map(g => (g.id, g)).toMap)).getOrElse(Map()) + + features.map(feature => { + extractProjectAndName(feature, projectChoiceStrategy) + .flatMap { case (project, name) => + feature.toFeature(project, ZoneId.of(timezone), globalScriptById).map { + case (feature, maybeScript) => (feature.withName(name), maybeScript) + } + } + }) + }) + ) + ) + + val users = files.get("users") + .map(uri => readFile(uri, oldUserReads)) + + val keys = files.get("keys") + .map(uri => readFile(uri, oldKeyReads)) + + val errors = maybeEitherFeatures.map(e => e.swap.getOrElse(Seq())).toSeq.flatten + + val eitherData = + if (Seq(maybeEitherFeatures, keys, users).forall(_.isEmpty)) { + Left(Seq("No file provided, nothing to import")) + } else if (errors.nonEmpty) { + Left(errors) + } else { + val projects = if (deduceProject) { + maybeEitherFeatures.flatMap(_.toOption.map(_.map(_._1.project))).getOrElse(Seq()).toSet + } else { + Set(project.get) + } + val maybeEitherUsers = users.map(t => unnest(t.map(users => users.map(u => toNewUser(tenant, u, projects, deduceProject))))) + val maybeEitherKeys = keys.map(t => unnest(t.map(keys => keys.map(k => Right(toNewKey(tenant, k, projects, deduceProject)))))) + + + val errors = Seq(maybeEitherUsers, maybeEitherKeys, maybeEitherScripts).foldLeft(Seq[String]()) { + case (s, Some(Left(errors))) => s.concat(errors) + case (s, _) => s + } + + val oldScripts = maybeEitherFeatures.flatMap(_.toOption).toSeq.flatten.flatMap { + case (_, None) => None + case (feature, Some(s)) => { + val id = s"${feature.id}_script" + Some(OldGlobalScript(id = id, name = id, source = s, description = None)) + } + }.concat(maybeEitherScripts.map(_.getOrElse(Seq())).getOrElse(Seq())) + + val (compatibleScripts, incompatibleScript) = oldScripts.partition(s => { + val isLangageSupported = s.source.language == JavaScript + val hasJsHttpCall = if(s.source.language == JavaScript) { + doesUseHttp(s.source.script) + } else { + false + } + isLangageSupported && !hasJsHttpCall + }) + + + if (errors.nonEmpty) { + Left(errors) + } else { + Right(MigrationData( + features = maybeEitherFeatures + .flatMap(either => either.toOption).toSeq + .flatMap(s => s.map(tuple => tuple._1)) + .map { + case WasmFeature(id, name, project, enabled, WasmConfig(scriptName, _, _, _, _, _, _, _, _, _, _, _), tags, metadata, description) + if !compatibleScripts.exists(s => s.id == scriptName) => Feature(id=id, name=name, project=project, enabled=enabled, tags=tags, metadata=metadata, description=description, conditions=Set()) + case f@_ => f + }, + keys = maybeEitherKeys.map(_.getOrElse(Seq())).getOrElse(Seq()), + users = maybeEitherUsers.map(_.getOrElse(Seq())).getOrElse(Seq()), + scripts = compatibleScripts, + excludedScripts = incompatibleScript + )) + } + } + + (strategy, eitherData) match { + case (None, _) => ImportFailure(id, Seq("Unknown conflict handling strategy")).future + case (_, Left(errors)) => ImportFailure(id, errors).future + case (Some(conflictStrategy), Right(MigrationData(features, users, keys, scripts, excludedScripts))) => { + env.postgresql.executeInTransaction(conn => { + scripts + .foldLeft(Future.successful[Either[IzanamiError, Map[String, (String, ByteString)]]](Right(Map())))((f, script) => { + f.flatMap { + case Right(ids) => wasmManagerClient.transferLegacyJsScript(script.id, script.source.script, local=isBase64).map { + case Left(error) => Left(error) + case Right((id, wasm)) => Right(ids + (script.id -> (id, wasm))) + } + case left => Future.successful(left) + } + }).flatMap { + case Left(err) => ImportFailure(id, Seq(err.message)).future + case Right(scriptIds) => { + val featureWithCorrectPath = features.map { + case f@WasmFeature(id, name, project, enabled, w@WasmConfig( + scriptName, + source, + memoryPages, + functionName, + config, + allowedHosts, + allowedPaths, + wasi, + opa, + instances, + killOptions, + authorizations + ), tags, metadata, description) => f.copy(wasmConfig = w.copy(source = w.source.copy(kind=if(isBase64) Base64 else Wasmo, path = if(isBase64) java.util.Base64.getEncoder.encodeToString(scriptIds(scriptName)._2.toArray) else scriptIds(scriptName)._1))) + case f => f + } + + env.datastores.features + .createFeaturesAndProjects(tenant, featureWithCorrectPath, conflictStrategy, user=request.user, conn = Some(conn)) + .flatMap { + case Left(errors) => Left(errors).future + case Right(_) => + env.datastores.apiKeys + .createApiKeys(tenant, keys, request.user, conflictStrategy, conn.some) + } + .flatMap { + case Left(err) => ImportFailure(id, err.map(err => err.message)).future + case Right(_) => + env.datastores.users + .createUserWithConn(users, conn, conflictStrategy) + .map { + case Left(error) => ImportFailure(id, Seq(error.message)) + case Right(_) => ImportSuccess( + id = id, + features = features.size, + users = users.size, + scripts = scripts.size, + keys = keys.size, + incompatibleScripts = excludedScripts.map(s => s.id) + ) + } + } + + } + } + }) + } + } + } + + env.datastores.tenants.markImportAsStarted() + .map { + case Left(err) => err.toHttpResponse + case Right(id) => { + Future { + runImport(id) + .map { + case s@ImportSuccess(id, features, users, scripts, keys, incompatibleScripts) => env.datastores.tenants.markImportAsSucceded(id, s) + case f@ImportFailure(id, errors) => env.datastores.tenants.markImportAsFailed(id, f) + }.recover(t => { + env.datastores.tenants.markImportAsFailed(id, ImportFailure(id, Seq(t.getMessage))) + }) + } + Accepted(Json.obj("id" -> id.toString)) + } + } + } +} + +object ImportController { + sealed trait ImportConflictStrategy + case object Fail extends ImportConflictStrategy + case object MergeOverwrite extends ImportConflictStrategy + case object Skip extends ImportConflictStrategy + + def sequence[A, B](seq: Seq[Either[A, B]]): Either[A, Seq[B]] = seq.foldRight(Right(Nil): Either[A, List[B]]) { + (e, acc) => for (xs <- acc; x <- e) yield x :: xs + } + + def scriptIdToNodeCompatibleName(name: String): String = { + name.replace("[", "-") + .replace("~", "-") + .replace(")", "-") + .replace("(", "-") + .replace("'", "-") + .replace("!", "-") + .replace("*", "-") + .replace(" ", "-") + .replace("]", "-") + .replace(":", "-") + .toLowerCase + match { + case str if str.startsWith("\\.") => str.replaceFirst("\\.", "") + case str if str.startsWith("_") => str.replaceFirst("_", "") + case str => str + } + } + + private def readFile[T](uri: URI, reads: Reads[T]): Try[Seq[T]] = { + Try(Source.fromFile(uri)) + .map(bf => + bf.getLines() + .map(line => { + val result = Json.parse(line).as[T](reads) + result + }) + .toSeq + ).recover(ex => { + throw ex + }) + } + + def parseStrategy(str: String): Option[ImportConflictStrategy] = { + Option(str).map(_.toUpperCase).flatMap { + case "OVERWRITE" => Some(MergeOverwrite) + case "SKIP" => Some(Skip) + case "FAIL" => Some(Fail) + case _ => None + } + } + + def extractProjectAndName(oldFeature: OldFeature, strategy: ProjectChoiceStrategy): Either[String, (String, String)] = { + strategy match { + case FixedProject(project) => Right((project, oldFeature.id)) + case DeduceProject(fieldCount) => { + val id = oldFeature.id + val parts = id.split(":") + if (parts.length <= fieldCount) { + Left(s"Feature ${id} is too short to exclude its first ${fieldCount} parts as project name") + } else { + (Right(parts.take(fieldCount).mkString(":"), parts.drop(fieldCount).mkString(":"))) + } + } + } + } + + def unnest[R](value: Try[Seq[Either[String, R]]]): Either[Seq[String], Seq[R]] = { + value.toEither.left + .map(t => Seq("Failed to read file")) + .flatMap(eithers => { + eithers.foldLeft(Right(Seq()): Either[Seq[String], Seq[R]])((either, next) => { + (either, next) match { + case (Left(errors), Left(other)) => Left(errors.appended(other)) + case (Left(errors), _) => Left(errors) + case (Right(rs), Right(r)) => Right(rs.appended(r)) + case (Right(_), Left(r)) => Left(Seq(r)) + } + }) + }) + } +} diff --git a/app/fr/maif/izanami/web/LegacyController.scala b/app/fr/maif/izanami/web/LegacyController.scala new file mode 100644 index 000000000..34a601fdb --- /dev/null +++ b/app/fr/maif/izanami/web/LegacyController.scala @@ -0,0 +1,74 @@ +package fr.maif.izanami.web + +import fr.maif.izanami.env.Env +import fr.maif.izanami.models.{Feature, RequestContext} +import fr.maif.izanami.utils.syntax.implicits.BetterSyntax +import play.api.libs.json.{JsObject, Json} +import play.api.mvc.{Action, AnyContent, BaseController, ControllerComponents} + +import scala.concurrent.{ExecutionContext, Future} + +class LegacyController +(val controllerComponents: ControllerComponents, +val clientKeyAction: ClientApiKeyAction) +(implicit val env: Env) + extends BaseController { + implicit val ec: ExecutionContext = env.executionContext + + def healthcheck(): Action[AnyContent] = Action.async { + Ok("").future + } + + def legacyFeature(pattern: String): Action[AnyContent] = clientKeyAction.async { + implicit request => { + val ctx = if(request.hasBody) { + val maybeObject = request.body.asJson.flatMap(js => js.asOpt[JsObject]) + val maybeId = maybeObject.flatMap(json => (json \ "id").asOpt[String]) + + RequestContext(tenant=request.key.tenant, user=maybeId.getOrElse(""), data = maybeObject.getOrElse(Json.obj())) + } else { + RequestContext(tenant=request.key.tenant, user="") + } + env.datastores.features.findByIdForKeyWithoutCheck(request.key.tenant, pattern, request.key.clientId) + .flatMap { + case Left(value) => Future.successful(value.toHttpResponse) + case Right(Some(feature)) => Feature.writeFeatureForCheckInLegacyFormat(feature, ctx, env).map { + case Right(Some(jsValue)) => Ok(jsValue) + case Right(None) => BadRequest(Json.obj("message" -> s"Feature $pattern is not a legacy compatible feature")) + case Left(error) => error.toHttpResponse + } + case Right(None) => Future.successful(NotFound(Json.obj("message" -> s"No feature $pattern"))) + } + } + } + + def legacyFeatures(pattern: String, active: Boolean, page: Int, pageSize: Int): Action[AnyContent] = clientKeyAction.async { + implicit request => { + val ctx = if (request.hasBody) { + val maybeObject = request.body.asJson.flatMap(js => js.asOpt[JsObject]) + val maybeId = maybeObject.flatMap(json => (json \ "id").asOpt[String]) + + RequestContext(tenant = request.key.tenant, user = maybeId.getOrElse(""), data = maybeObject.getOrElse(Json.obj())) + } else { + RequestContext(tenant = request.key.tenant, user = "") + } + env.datastores.features.findFeatureMatching(request.key.tenant, pattern, request.key.clientId, count=pageSize, page=page) + .flatMap{case (totalCount, features) => Future.sequence(features.map(feature => + Feature.writeFeatureForCheckInLegacyFormat(feature, ctx, env) + )) + .map(eitherMaybeJsons => { + eitherMaybeJsons.map { + case Left(error) => Json.obj("error" -> error.message) + case Right(None) => Json.obj("error" -> "This feature is not a legacy feature") + case Right(Some(json)) => json + } + }) + .map(res => (totalCount, res))} + .map{case (totalCount, jsonResult) => { + Json.obj("results" -> jsonResult, "metadata" -> Json.obj("count" -> totalCount, "page" -> page, "pageSize" -> pageSize, "nbPages" -> Math.ceil(totalCount.toFloat / pageSize))) + }} + .map(jsonResult => Ok(Json.toJson(jsonResult))) + } + } + +} diff --git a/app/fr/maif/izanami/web/LoginController.scala b/app/fr/maif/izanami/web/LoginController.scala new file mode 100644 index 000000000..330acf653 --- /dev/null +++ b/app/fr/maif/izanami/web/LoginController.scala @@ -0,0 +1,139 @@ +package fr.maif.izanami.web + +import fr.maif.izanami.env.Env +import fr.maif.izanami.errors.MissingOIDCConfigurationError +import fr.maif.izanami.models.User.userRightsWrites +import fr.maif.izanami.models.{OIDC, OIDCConfiguration, Rights, User} +import fr.maif.izanami.utils.syntax.implicits.BetterSyntax +import pdi.jwt.{JwtJson, JwtOptions} +import play.api.libs.json.Json +import play.api.libs.ws.WSAuthScheme +import play.api.mvc.Cookie.SameSite +import play.api.mvc._ + +import java.util.Base64 +import scala.concurrent.{ExecutionContext, Future} + +class LoginController( + val env: Env, + val controllerComponents: ControllerComponents, + sessionAuthAction: AuthenticatedSessionAction +) extends BaseController { + implicit val ec: ExecutionContext = env.executionContext; + + def openIdConnect = Action { + env.datastores.configuration.readOIDCConfiguration() match { + case None => MissingOIDCConfigurationError().toHttpResponse + case Some(OIDCConfiguration(clientId, _, authorizeUrl, _, redirectUrl)) => Redirect(s"${authorizeUrl}?scope=openid%20profile%20email%20name&client_id=${clientId}&response_type=code&redirect_uri=${redirectUrl}") + } + } + + def openIdCodeReturn = Action.async { implicit request => + // TODO handle refresh_token + { + for ( + code <- request.body.asJson.flatMap(json => (json \ "code").get.asOpt[String]); + OIDCConfiguration(clientId, clientSecret, _, tokenUrl, redirectUrl) <- env.datastores.configuration.readOIDCConfiguration() + ) + yield env.Ws + .url(tokenUrl) + .withAuth(clientId, clientSecret, WSAuthScheme.BASIC) + .withHttpHeaders(("content-type", "application/x-www-form-urlencoded")) + .post(Map("grant_type" -> "authorization_code", "code" -> code, "redirect_uri" -> redirectUrl)) + //.post(s"grant_type=authorization_code&code=${code}&redirect_uri=${redirectUrl}") + .flatMap(r => { + val maybeToken = (r.json \ "id_token").get.asOpt[String] + maybeToken.fold(Future(InternalServerError(Json.obj("message" -> "Failed to retrieve token"))))(token => { + val maybeClaims = JwtJson.decode(token, JwtOptions(signature = false)) + maybeClaims.toOption + .flatMap(claims => claims.subject) + .map(userId => + env.datastores.users + .findUser(userId) + .flatMap(maybeUser => + maybeUser.fold( + // TODO handle mail + env.datastores.users.createUser(User(userId, userType = OIDC).withRights(Rights.EMPTY)) + )(user => Future(Right(user.withRights(Rights.EMPTY)))).map(either => either.map(_ => userId)) + ) + ) + .getOrElse(Future(Left(InternalServerError(Json.obj("message" -> "Failed to read token claims"))))) + .flatMap { + // TODO refactor this whole method + case Right(username) => env.datastores.users.createSession(username).map(id => Right(id)) + case Left(err) => Future(Left(err)) + } + .map(maybeId => { + maybeId + .map(id => { + env.jwtService.generateToken(id) + }) + .map(token => + NoContent + .withCookies( + Cookie(name = "token", value = token, httpOnly = false, sameSite = Some(SameSite.Strict)) + ) + ) + .getOrElse(InternalServerError(Json.obj("message" -> "Failed to read token claims"))) + }) + }) + }) + }.getOrElse(Future(InternalServerError(Json.obj("message" -> "Failed to read token claims")))) + } + + def logout() = sessionAuthAction.async { implicit request => + env.datastores.users + .deleteSession(request.sessionId) + .map(_ => { + NoContent.withCookies( + Cookie( + name = "token", + value = "", + httpOnly = false, + sameSite = Some(SameSite.Strict), + maxAge = Some(0) + ) + ) + }) + } + + def login(rights: Boolean = false): Action[AnyContent] = Action.async { implicit request => + request.headers + .get("Authorization") + .map(header => header.split("Basic ")) + .filter(splitted => splitted.length == 2) + .map(splitted => splitted(1)) + .map(header => { + Base64.getDecoder.decode(header.getBytes) + }) + .map(bytes => new String(bytes)) + .map(header => header.split(":")) + .filter(arr => arr.length == 2) match { + case Some(Array(username, password, _*)) => + env.datastores.users.isUserValid(username, password).flatMap { + case None => Future.successful(Forbidden(Json.obj("message" -> "Incorrect credentials"))) + case Some(user) => + for { + _ <- if (user.legacy) env.datastores.users.updateLegacyUser(username, password) + else Future.successful(()) + sessionId <- env.datastores.users.createSession(user.username) + token <- env.jwtService.generateToken(sessionId).future + response <- if (rights) env.datastores.users.findUserWithCompleteRights(user.username).map { + case Some(user) => Ok(Json.toJson(user)(userRightsWrites)) + case None => InternalServerError(Json.obj("message" -> "Failed to read rights")) + } + else Future.successful(Ok) + } yield response.withCookies( + Cookie( + name = "token", + value = token, + httpOnly = false, + sameSite = Some(SameSite.Strict), + maxAge = Some(env.configuration.get[Int]("app.sessions.ttl") - 120) + ) + ) + } + case _ => Future(Unauthorized(Json.obj("message" -> "Missing credentials"))) + } + } +} diff --git a/app/fr/maif/izanami/web/PluginController.scala b/app/fr/maif/izanami/web/PluginController.scala new file mode 100644 index 000000000..2aaaae6e2 --- /dev/null +++ b/app/fr/maif/izanami/web/PluginController.scala @@ -0,0 +1,102 @@ +package fr.maif.izanami.web + +import fr.maif.izanami.env.Env +import fr.maif.izanami.models.RightLevels +import fr.maif.izanami.utils.syntax.implicits.BetterSyntax +import fr.maif.izanami.wasm.{WasmConfig, WasmConfigWithFeatures} +import io.otoroshi.wasm4s.scaladsl.WasmoSettings +import play.api.libs.json.{JsValue, Json} +import play.api.mvc._ + +import scala.concurrent.ExecutionContext +import scala.util.{Failure, Success, Try} + +class PluginController( + val env: Env, + val controllerComponents: ControllerComponents, + val authAction: TenantAuthActionFactory, + val adminAuthAction: AdminAuthAction +) extends BaseController { + implicit val ec: ExecutionContext = env.executionContext; + + // TODO authenticate + def localScripts(tenant: String, features: Boolean) = Action.async { implicit request => + // FIXME json mapping :'( + if (features) { + env.datastores.features + .readLocalScriptsWithAssociatedFeatures(tenant) + .map(configs => + Ok(Json.toJson(configs.map(w => Json.toJson(w)(WasmConfigWithFeatures.wasmConfigWithFeaturesWrites)))) + ) + } else { + env.datastores.features + .readLocalScripts(tenant) + .map(configs => Ok(Json.toJson(configs.map(w => Json.toJson(w)(WasmConfig.format))))) + } + } + + def deleteScript(tenant: String, script: String): Action[AnyContent] = authAction(tenant, RightLevels.Write).async { + implicit request => + env.datastores.features.deleteLocalScript(tenant, script).map { + case Left(err) => err.toHttpResponse + case Right(_) => NoContent + } + } + + def updateScript(tenant: String, script: String): Action[JsValue] = + authAction(tenant, RightLevels.Write).async(parse.json) { implicit request => + request.body.asOpt[WasmConfig](WasmConfig.format) match { + case Some(value) => env.datastores.features.updateWasmScript(tenant, script, value).map(_ => NoContent) + case None => BadRequest(Json.obj("message" -> "Bad body format")).future + } + } + + // TODO basic authentication + def wasmFiles() = Action.async { implicit request => + env.datastores.configuration + .readWasmConfiguration() match { + case Some(settings @ WasmoSettings(url, _, _, pluginsFilter, _)) => + Try { + val userHeader = io.otoroshi.wasm4s.scaladsl.ApikeyHelper.generate(settings) + env.Ws + .url(s"$url/plugins") + .withFollowRedirects(false) + .withHttpHeaders( + "Accept" -> "application/json", + userHeader, + "kind" -> pluginsFilter.getOrElse("*") + ) + .get() + .map(res => { + if (res.status == 200) { + Ok(res.json) + } else { + Ok(Json.arr()) + } + }) + .recover { case e: Throwable => + env.logger.error(s"Failed to retrieve wasm scripts", e) + Ok(Json.arr()) + } + } match { + case Failure(err) => { + env.logger.error(s"Failed to retrieve wasm scripts", err) + Ok(Json.arr()).future + } + case Success(v) => v + } + + case _ => + BadRequest( + Json.obj( + "message" -> "Missing config in global configuration" + ) + ).future + } + } + + def clearWasmCache(): Action[AnyContent] = adminAuthAction.async { implicit request => + env.wasmIntegration.context.wasmScriptCache.clear().future.map(_ => NoContent) + } + +} diff --git a/app/fr/maif/izanami/web/ProjectController.scala b/app/fr/maif/izanami/web/ProjectController.scala new file mode 100644 index 000000000..018df5daa --- /dev/null +++ b/app/fr/maif/izanami/web/ProjectController.scala @@ -0,0 +1,81 @@ +package fr.maif.izanami.web + +import fr.maif.izanami.env.Env +import fr.maif.izanami.models.RightLevels.RightLevel +import fr.maif.izanami.models.{Project, RightLevels} +import fr.maif.izanami.utils.syntax.implicits.BetterSyntax +import play.api.libs.json.{JsError, JsSuccess, JsValue, Json} +import play.api.mvc._ + +import scala.concurrent.ExecutionContext + +class ProjectController( + val env: Env, + val controllerComponents: ControllerComponents, + val tenantAuthAction: TenantAuthActionFactory, + val projectAuthAction: ProjectAuthActionFactory, + val detailledRightForTenanFactory: DetailledRightForTenantFactory +) extends BaseController { + implicit val ec: ExecutionContext = env.executionContext; + + def createProject(tenant: String): Action[JsValue] = tenantAuthAction(tenant, RightLevels.Write).async(parse.json) { implicit request => + Project.projectReads.reads(request.body) match { + case JsError(e) => BadRequest(Json.obj("message" -> "bad body format")).future + case JsSuccess(project, _) => { + env.datastores.projects + .createProject(tenant, project, request.user) + .map(maybeProject => + maybeProject.fold( + err => Results.Status(err.status)(Json.toJson(err)), + project => Created(Json.toJson(project)) + ) + ) + } + } + } + + def updateProject(tenant: String, project: String): Action[JsValue] = projectAuthAction(tenant, project, RightLevels.Admin).async(parse.json) { implicit request => + Project.projectReads.reads(request.body) match { + case JsSuccess(updatedProject, _) => env.datastores.projects.updateProject(tenant, project, updatedProject).map(_ => NoContent) + case JsError(_) => BadRequest(Json.obj("message" -> "bad body format")).future + } + } + + def readProjects(tenant: String): Action[AnyContent] = detailledRightForTenanFactory(tenant).async { implicit request => + val isTenantAdmin = request.user.tenantRight.exists(right => right.level == RightLevels.Admin) + if(request.user.admin || isTenantAdmin) { + env.datastores.projects + .readProjects(tenant) + .map(projects => Ok(Json.toJson(projects))) + } else { + val filter = request.user.tenantRight + .map(tr => tr.projects.keys.toSet) + .getOrElse(Set()) + env.datastores.projects + .readProjectsFiltered(tenant, filter) + .map(projects => Ok(Json.toJson(projects))) + } + + } + + def readProject(tenant: String, project: String): Action[AnyContent] = projectAuthAction(tenant, project, RightLevels.Read).async { + implicit request => + env.datastores.projects + .readProject(tenant, project) + .map(maybeProject => { + maybeProject.fold( + err => Results.Status(err.status)(Json.toJson(err)), + project => Ok(Json.toJson(project)) + ) + }) + } + + def deleteProject(tenant: String, project: String): Action[AnyContent] = projectAuthAction(tenant, project, RightLevels.Admin).async { + implicit request => + env.datastores.projects + .deleteProject(tenant, project).map { + case Left(err) => err.toHttpResponse + case Right(value) => NoContent + } + } +} diff --git a/app/fr/maif/izanami/web/TagController.scala b/app/fr/maif/izanami/web/TagController.scala new file mode 100644 index 000000000..e4791d68b --- /dev/null +++ b/app/fr/maif/izanami/web/TagController.scala @@ -0,0 +1,59 @@ +package fr.maif.izanami.web + +import fr.maif.izanami.env.Env +import fr.maif.izanami.models.{RightLevels, Tag} +import fr.maif.izanami.utils.syntax.implicits.BetterSyntax +import play.api.libs.json.{JsError, JsSuccess, JsValue, Json} +import play.api.mvc._ + +import scala.concurrent.{ExecutionContext, Future} + +class TagController( + val env: Env, + val controllerComponents: ControllerComponents, + val authAction: TenantAuthActionFactory +) extends BaseController { + implicit val ec: ExecutionContext = env.executionContext; + + def createTag(tenant: String): Action[JsValue] = authAction(tenant, RightLevels.Write).async(parse.json) { + implicit request => + Future.successful(Forbidden) + Tag.tagRequestReads.reads(request.body) match { + case JsError(e) => BadRequest(Json.obj("message" -> "bad body format")).future + case JsSuccess(tag, _) => { + env.datastores.tags + .createTag(tag, tenant) + .map(maybeTenant => + maybeTenant.fold( + err => Results.Status(err.status)(Json.toJson(err)), + tag => Created(Json.toJson(tag)) + ) + ) + } + } + } + + def deleteTag(tenant: String, name: String): Action[AnyContent] = authAction(tenant, RightLevels.Write).async { + implicit request: Request[AnyContent] => env.datastores.tags.deleteTag(tenant, name).map { + case Left(err) => err.toHttpResponse + case Right(value) => NoContent + } + } + + def readTag(tenant: String, name: String): Action[AnyContent] = authAction(tenant, RightLevels.Read).async { + implicit request: Request[AnyContent] => + env.datastores.tags + .readTag(tenant, name) + .map(maybeTag => + maybeTag.fold( + err => Results.Status(err.status)(Json.toJson(err)), + tag => Ok(Json.toJson(tag)) + ) + ) + } + + def readTags(tenant: String): Action[AnyContent] = authAction(tenant, RightLevels.Read).async { implicit request: Request[AnyContent] => + env.datastores.tags.readTags(tenant).map(tags => Ok(Json.toJson(tags))) + } + +} diff --git a/app/fr/maif/izanami/web/TenantController.scala b/app/fr/maif/izanami/web/TenantController.scala new file mode 100644 index 000000000..99c61bebc --- /dev/null +++ b/app/fr/maif/izanami/web/TenantController.scala @@ -0,0 +1,111 @@ +package fr.maif.izanami.web + +import fr.maif.izanami.env.Env +import fr.maif.izanami.models.RightLevels.{RightLevel, superiorOrEqualLevels} +import fr.maif.izanami.models._ +import fr.maif.izanami.utils.syntax.implicits.BetterSyntax +import fr.maif.izanami.v1.WasmManagerClient +import play.api.libs.json._ +import play.api.mvc._ + +import scala.concurrent.{ExecutionContext, Future} + +sealed trait ProjectChoiceStrategy +case class DeduceProject(fieldCount: Int = 1) extends ProjectChoiceStrategy +case class FixedProject(name: String) extends ProjectChoiceStrategy + +class TenantController( + val env: Env, + val controllerComponents: ControllerComponents, + val tenantAuthAction: TenantAuthActionFactory, + val adminAuthAction: AdminAuthAction, + val tenantRightsAuthAction: TenantRightsAction, + val wasmManagerClient: WasmManagerClient +) extends BaseController { + implicit val ec: ExecutionContext = env.executionContext; + + def updateTenant(name: String): Action[JsValue] = tenantAuthAction(name, RightLevels.Admin).async(parse.json) { + implicit request => + Tenant.tenantReads.reads(request.body) match { + case JsSuccess(value, _) => + if(name != value.name) { + BadRequest(Json.obj("message" -> "Modification of a tenant name is not permitted")).future + } else { + env.datastores.tenants.updateTenant(name, value).map { + case Left(err) => err.toHttpResponse + case Right(value) => NoContent + } + } + case JsError(errors) => BadRequest(Json.obj("message" -> "Bad body format")).future + } + } + + def createTenant(): Action[JsValue] = adminAuthAction.async(parse.json) { implicit request => + Tenant.tenantReads.reads(request.body) match { + case JsError(e) => BadRequest(Json.obj("message" -> "bad body format")).future + case JsSuccess(tenant, _) => { + env.datastores.tenants + .createTenant(tenant, request.user) + .map(maybeTenant => + maybeTenant.fold( + err => Results.Status(err.status)(Json.toJson(err)), + tenant => Created(Json.toJson(tenant)) + ) + ) + } + } + } + + def readTenants(right: Option[RightLevel]): Action[AnyContent] = tenantRightsAuthAction.async { implicit request => + if (request.user.admin) { + env.datastores.tenants + .readTenants() + .map(tenants => Ok(Json.toJson(tenants))) + } else { + val minimumRightLevel = right.getOrElse(RightLevels.Read) + val allowedTenants = Option(request.user.tenantRights) + .map(m => + m.filter { case (name, level) => superiorOrEqualLevels(minimumRightLevel).contains(level) }.keys.toSet + ) + .getOrElse(Set()) + env.datastores.tenants + .readTenantsFiltered(allowedTenants) + .map(tenants => Ok(Json.toJson(tenants))) + } + } + + def deleteTenant(name: String): Action[AnyContent] = tenantAuthAction(name, RightLevels.Admin).async { + implicit request => + env.datastores.tenants.deleteTenant(name).map { + case Left(err) => err.toHttpResponse + case Right(value) => NoContent + } + } + + def readTenant(name: String): Action[AnyContent] = tenantAuthAction(name, RightLevels.Read).async { + implicit request => + env.datastores.tenants + .readTenantByName(name) + .flatMap(maybeTenant => + maybeTenant.fold( + err => Future.successful(Results.Status(err.status)(Json.toJson(err))), + tenant => { + for ( + projects <- { + env.datastores.projects.readTenantProjectForUser(tenant.name, request.user) + }; + tags <- env.datastores.tags.readTags(tenant.name) + ) + yield Ok( + Json.toJson( + Tenant(name = tenant.name, projects = projects, tags = tags, description = tenant.description) + ) + ) + } + ) + ) + } +} + + + diff --git a/app/fr/maif/izanami/web/UserController.scala b/app/fr/maif/izanami/web/UserController.scala new file mode 100644 index 000000000..61427e3ff --- /dev/null +++ b/app/fr/maif/izanami/web/UserController.scala @@ -0,0 +1,453 @@ +package fr.maif.izanami.web + +import fr.maif.izanami.env.Env +import fr.maif.izanami.errors.{BadBodyFormat, EmailAlreadyUsed} +import fr.maif.izanami.models.RightLevels.RightLevel +import fr.maif.izanami.models.Rights.TenantRightDiff +import fr.maif.izanami.models.User._ +import fr.maif.izanami.models._ +import fr.maif.izanami.utils.syntax.implicits.BetterSyntax +import fr.maif.izanami.v1.OldUsers.oldUserReads +import fr.maif.izanami.v1.{OldUser, OldUsers} +import play.api.data.validation.{Constraints, Valid} +import play.api.libs.json._ +import play.api.mvc._ + +import java.util.Objects +import scala.concurrent.{ExecutionContext, Future} +import scala.io.Source +import scala.util.{Failure, Success, Try} + +class UserController( + val env: Env, + val controllerComponents: ControllerComponents, + val authAction: AuthenticatedAction, + val adminAction: AdminAuthAction, + val detailledAuthAction: DetailledAuthAction, + val tenantRightsAction: TenantRightsAction, + val tenantRightFilterAction: TenantAuthActionFactory, + val projectAuthAction: ProjectAuthActionFactory +) extends BaseController { + implicit val ec: ExecutionContext = env.executionContext; + + def hasRight(loggedInUser: UserWithTenantRights, admin: Boolean, rights: Rights): Boolean = { + val loggedInUserTenantsAdmin = loggedInUser.tenantRights.filter { case (_, right) => + right == RightLevels.Admin + }.keySet + if (!loggedInUser.admin && loggedInUserTenantsAdmin.isEmpty) { + false + } else if (admin) { + loggedInUser.admin + } else { + val tenants = rights.tenants.keySet + loggedInUser.admin || tenants.subsetOf(loggedInUserTenantsAdmin) + } + } + + def sendInvitation(): Action[JsValue] = tenantRightsAction.async(parse.json) { implicit request => + { + def handleInvitation(email: String, id: String) = { + val token = env.jwtService.generateToken( + id, + Json.obj("invitation" -> id) + ) + + env.datastores.configuration + .readConfiguration() + .flatMap { + case Left(err) => err.toHttpResponse.future + case Right(configuration) if configuration.invitationMode == InvitationMode.Response => { + Created(Json.obj("invitationUrl" -> s"""${env.expositionUrl}/invitation?token=${token}""")).future + } + case Right(configuration) if configuration.invitationMode == InvitationMode.Mail => { + env.mails + .sendInvitationMail(email, token) + .map(futureResult => futureResult.fold(err => InternalServerError(Json.obj("message" -> err.message)), _ => NoContent)) + } + case Right(c) => throw new RuntimeException("Unknown invitation mode " + c.invitationMode) + } + + } + + User.userInvitationReads + .reads(request.body) + .fold( + _ => Future.successful(Left(BadRequest("Invalid Payload"))), + invitation => + env.datastores.users + .findUserByMail(invitation.email) + .map(maybeUser => + maybeUser.map(_ => EmailAlreadyUsed(invitation.email).toHttpResponse).toLeft(invitation) + ) + ) + .map { + case Right(invitation) if hasRight(request.user, invitation.admin, invitation.rights) => Right(invitation) + case Right(_) => Left(Forbidden(Json.obj("message" -> "Not enough rights"))) + case left => left + } + .flatMap(e => { + e.fold( + r => r.future, + invitation => + env.datastores.users + .createInvitation(invitation.email, invitation.admin, invitation.rights, request.user.username) + .flatMap(either => + either.fold(err => err.toHttpResponse.future, id => handleInvitation(invitation.email, id)) + ) + ) + }) + } + } + + def updateUser(user: String): Action[JsValue] = authAction.async(parse.json) { implicit request => + if (!request.user.equalsIgnoreCase(user)) { + Forbidden(Json.obj("message" -> "Modification of other users information is not allowed")).future + } else { + // TODO make special action that check password ? + User.userUpdateReads.reads(request.body) match { + case JsSuccess(updateRequest, _) => { + env.datastores.users + .isUserValid(user, updateRequest.password) + .flatMap { + case Some(user) => { + env.datastores.users.updateUserInformation(user.username, updateRequest).map { + case Left(err) => err.toHttpResponse + case Right(_) => NoContent + } + } + case None => Unauthorized(Json.obj("message" -> "Wrong username / password")).future + } + } + case JsError(_) => BadBodyFormat().toHttpResponse.future + } + } + } + + def updateUserRightsForProject(tenant: String, project: String, user: String): Action[JsValue] = + projectAuthAction(tenant, project, RightLevels.Admin).async(parse.json) { implicit request => + request.body + .asOpt[JsObject] + .fold(BadBodyFormat().toHttpResponse.future)(obj => { + if (obj.fields.isEmpty) { + env.datastores.users.deleteRightsForProject(user, tenant, project).map(_ => NoContent) + } else { + val newLevel = (obj \ "level").as[RightLevel] + + env.datastores.users.findUser(user).flatMap { + case Some(userWithTenantRights) => + { + userWithTenantRights.tenantRights.get(tenant) match { + case Some(_) => env.datastores.users.updateUserRightsForProject(user, tenant, project, newLevel) + case None => + env.datastores.users.updateUserRightsForTenant( + user, + tenant, + TenantRightDiff( + addedTenantRight = Some(Rights.FlattenTenantRight(tenant, RightLevels.Read)), + addedProjectRights = Set(Rights.FlattenProjectRight(project, tenant, level = newLevel)) + ) + ) + } + }.map(_ => NoContent) + case None => NotFound(Json.obj("message" -> "user not found")).future + } + } + }) + } + + def updateUserRights(user: String): Action[JsValue] = adminAction.async(parse.json) { implicit request => + User.userRightsUpdateReads.reads(request.body) match { + case JsSuccess(modificationRequest, _) => + env.datastores.users.updateUserRights(user, modificationRequest).map { + case Left(err) => err.toHttpResponse + case Right(_) => NoContent + } + case JsError(_) => BadBodyFormat().toHttpResponse.future + } + } + + def updateUserRightsForTenant(tenant: String, user: String): Action[JsValue] = { + // TODO use tenantActionRight ? + detailledAuthAction.async(parse.json) { implicit request => + if ((request.body.as[JsObject]).fields.isEmpty) { + env.datastores.users.deleteRightsForTenant(user, tenant, request.user).map { + case Left(err) => err.toHttpResponse + case Right(value) => NoContent + } + } else { + User.tenantRightReads.reads(request.body) match { + case JsSuccess(value, _) => { + env.datastores.users.findUserWithCompleteRights(user).flatMap { + case Some(user) => { + val currentRights: TenantRight = user.rights.tenants.getOrElse(tenant, TenantRight(null)) + val diff = Rights.compare(tenant, base = Option(currentRights), modified = Option(value)) + + diff match { + case None => NoContent.future + case Some(diff) => { + // TODO externalize this + val authorized = + diff.removedProjectRights + .concat(diff.addedProjectRights) + .map(_.name) + .forall(project => request.user.hasAdminRightForProject(project, tenant)) && + diff.removedKeyRights + .concat(diff.addedKeyRights) + .map(_.name) + .forall(key => request.user.hasAdminRightForKey(key, tenant)) && + diff.addedTenantRight + .orElse(diff.removedTenantRight) + .map(_.name) + .forall(tenant => request.user.hasAdminRightForTenant(tenant)) + if (!authorized) { + Forbidden(Json.obj("message" -> "Not enough rights")).future + } else { + env.datastores.users.updateUserRightsForTenant(user.username, tenant, diff).map(_ => NoContent) + } + } + } + } + case None => BadRequest(Json.obj("message" -> s"User ${user} does not exist")).future + } + } + case JsError(_) => BadBodyFormat().toHttpResponse.future + } + } + } + } + + def updateUserPassword(user: String): Action[JsValue] = authAction.async(parse.json) { implicit request => + if (!request.user.equalsIgnoreCase(user)) { + Forbidden("Modification of other users information is not allowed").future + } else { + // TODO check password during update + User.userPasswordUpdateReads.reads(request.body) match { + case JsSuccess(updateRequest, _) => { + env.datastores.users + .isUserValid(user, updateRequest.oldPassword) + .flatMap { + case Some(user) => { + env.datastores.users.updateUserPassword(user.username, updateRequest.password).map { + case Left(err) => err.toHttpResponse + case Right(value) => NoContent + } + } + case None => Unauthorized(Json.obj("message" -> "Wrong username / password")).future + } + } + case JsError(errors) => BadBodyFormat().toHttpResponse.future + } + } + } + + def resetPassword(): Action[JsValue] = Action.async(parse.json) { implicit request => + (request.body \ "email") + .asOpt[String] + .filter(Constraints.emailAddress.apply(_) == Valid) + .map(email => { + env.datastores.users + .findUserByMail(email) + .filter(_.forall(_.userType == INTERNAL)) + .flatMap { + case Some(user) => { + env.datastores.users + .savePasswordResetRequest(user.username) + .flatMap(id => { + val token = env.jwtService.generateToken( + id, + Json.obj("reset" -> id) + ) + env.mails.sendPasswordResetEmail(email, token).map(_ => NoContent) + }) + } + case None => NoContent.future + } + }) + .getOrElse(BadRequest("Bad body request").future) + } + + def createUser(): Action[JsValue] = Action.async(parse.json) { implicit request => + val result = + for ( + username <- + (request.body \ "username").asOpt[String].filter(name => NAME_REGEXP.pattern.matcher(name).matches()); + password <- + (request.body \ "password").asOpt[String].filter(name => PASSWORD_REGEXP.pattern.matcher(name).matches()); + token <- (request.body \ "token").asOpt[String]; + parsedToken <- env.jwtService.parseJWT(token).toOption; + content <- Option(parsedToken.content); + jsonContent <- Try { + Json.parse(content) + }.toOption; + invitation <- (jsonContent \ "invitation").asOpt[String] + ) yield { + env.datastores.users.readInvitation(invitation).flatMap { + case Some(invitation) => { + val user = UserWithRights( + username = username, + email = invitation.email, + password = password, + rights = invitation.rights, + admin = invitation.admin, + userType = INTERNAL + ) + env.datastores.users + .createUser(user) + .flatMap(eitherUser => { + eitherUser + .map(user => { + env.datastores.users.deleteInvitation(invitation.id).map { + _.map(_ => user).toRight(fr.maif.izanami.errors.InternalServerError()) + } + }) + .fold(err => Left(err).future, foo => foo) + }) + .map { + case Right(_) => Created(Json.toJson(user)) + case Left(error) => error.toHttpResponse + } + } + case None => NotFound(Json.obj("message" -> "Invitation not found")).future + } + } + result.getOrElse(BadBodyFormat().toHttpResponse.future) + } + + def readUsers(): Action[AnyContent] = authAction.async { implicit request => + env.datastores.users + .findUsers(request.user) + .map(users => { + Ok(Json.toJson(users)) + }) + } + + def searchUsers(query: String, count: Integer): Action[AnyContent] = authAction.async { implicit request => + var effectiveCount: Integer = Objects.requireNonNullElse(count, 10) + if (effectiveCount > 100) effectiveCount = 100 + env.datastores.users + .searchUsers(query, effectiveCount) + .map(usernames => Ok(Json.toJson(usernames))) + } + + def inviteUsersToProject(tenant: String, project: String): Action[JsValue] = + projectAuthAction(tenant, project, RightLevels.Admin).async(parse.json) { implicit request => + request.body + .asOpt[JsArray] + .map(arr => + arr.value + .map(value => { + for ( + username <- (value \ "username").asOpt[String]; + right <- (value \ "level").asOpt[RightLevel] + ) yield (username, right) + }) + .filter(_.isDefined) + .map(_.get) + .toSeq + ) match { + case Some(seq) => env.datastores.users.addUserRightsToProject(tenant, project, seq).map(_ => NoContent) + case None => BadBodyFormat().toHttpResponse.future + } + } + + def inviteUsersToTenant(tenant: String): Action[JsValue] = + tenantRightFilterAction(tenant, RightLevels.Admin).async(parse.json) { implicit request => + request.body + .asOpt[JsArray] + .map(arr => + arr.value + .map(value => { + for ( + username <- (value \ "username").asOpt[String]; + right <- (value \ "level").asOpt[RightLevel] + ) yield (username, right) + }) + .filter(_.isDefined) + .map(_.get) + .toSeq + ) match { + case Some(seq) => env.datastores.users.addUserRightsToTenant(tenant, seq).map(_ => NoContent) + case None => BadBodyFormat().toHttpResponse.future + } + } + + def readUser(user: String): Action[AnyContent] = adminAction.async { implicit request => + env.datastores.users + .findUserWithCompleteRights(user) + .map { + case Some(user) => Ok(Json.toJson(user)) + case None => NotFound(Json.obj("message" -> "user does not exist")) + } + } + + def readUserForTenant(tenant: String, user: String): Action[AnyContent] = + tenantRightFilterAction(tenant, RightLevels.Admin).async { implicit request => + env.datastores.users + .findCompleteRightsFromTenant(user, Set(tenant)) + .map { + case Some(user) => Ok(Json.toJson(user)) + case None => NotFound(Json.obj("message" -> "user does not exist")) + } + } + + def readUsersForTenant(tenant: String): Action[AnyContent] = + tenantRightFilterAction(tenant, RightLevels.Admin).async { implicit request => + env.datastores.users + .findUsersForTenant(tenant) + .map(users => Ok(Json.toJson(users))) + } + + def readUsersForProject(tenant: String, project: String): Action[AnyContent] = + projectAuthAction(tenant, project, RightLevels.Admin).async { implicit request => + env.datastores.users + .findUsersForProject(tenant, project) + .map(users => Ok(Json.toJson(users))) + } + + def deleteUser(user: String): Action[AnyContent] = adminAction.async { implicit request => + if (request.user.equals(user)) { + Future.successful(BadRequest(Json.obj("message" -> "User can't delete itself !"))) + } else { + env.datastores.users.deleteUser(user).map(_ => NoContent) + } + } + + def readRights(): Action[AnyContent] = authAction.async { implicit request => + env.datastores.users + .findUserWithCompleteRights(request.user) + .map { + case Some(user) => Ok(Json.toJson(user)(User.userRightsWrites)) + case None => NotFound(Json.obj("message" -> "User does not exist")) + } + } + + def reinitializePassword(): Action[JsValue] = Action.async(parse.json) { implicit request => + val result = + for ( + password <- + (request.body \ "password").asOpt[String].filter(name => PASSWORD_REGEXP.pattern.matcher(name).matches()); + token <- (request.body \ "token").asOpt[String]; + parsedToken <- env.jwtService.parseJWT(token).toOption; + content <- Option(parsedToken.content); + jsonContent <- Try { + Json.parse(content) + }.toOption; + reset <- (jsonContent \ "reset").asOpt[String] + ) yield { + env.datastores.users + .findPasswordResetRequest(reset) + .flatMap { + case Some(username) => { + env.datastores.users + .updateUserPassword(username, password) + .flatMap(_ => env.datastores.users.deletePasswordResetRequest(reset)) + .map(_ => NoContent) + } + case None => NotFound(Json.obj("message" -> "No password reset pending for this user")).future + } + } + + result.getOrElse(BadBodyFormat().toHttpResponse.future) + } + +} diff --git a/build.sbt b/build.sbt new file mode 100644 index 000000000..80ab96fd4 --- /dev/null +++ b/build.sbt @@ -0,0 +1,110 @@ +import ReleaseTransformations._ + +name := """izanami-v2""" +organization := "fr.maif" + +version := "1.0-SNAPSHOT" + +lazy val root = (project in file(".")).enablePlugins(PlayScala).enablePlugins(BuildInfoPlugin) + +lazy val excludesJackson = Seq( + ExclusionRule(organization = "com.fasterxml.jackson.core"), + ExclusionRule(organization = "com.fasterxml.jackson.datatype"), + ExclusionRule(organization = "com.fasterxml.jackson.dataformat") +) + +scalaVersion := "2.13.12" + +resolvers ++= Seq( + "jsonlib-repo" at "https://raw.githubusercontent.com/mathieuancelin/json-lib-javaslang/master/repository/releases", + Resolver.jcenterRepo +) + +libraryDependencies += guice +libraryDependencies += ws +libraryDependencies += "com.softwaremill.macwire" %% "macros" % "2.3.7" % "provided" + +libraryDependencies += "com.zaxxer" % "HikariCP" % "5.1.0" +libraryDependencies += "io.vertx" % "vertx-pg-client" % "4.5.1" +libraryDependencies += "com.ongres.scram" % "common" % "2.1" +libraryDependencies += "com.ongres.scram" % "client" % "2.1" +libraryDependencies += "org.flywaydb" % "flyway-database-postgresql" % "10.4.1" excludeAll (excludesJackson: _*) +libraryDependencies += "org.postgresql" % "postgresql" % "42.7.2" +libraryDependencies += "com.github.jwt-scala" %% "jwt-play-json" % "9.4.5" excludeAll (excludesJackson: _*) +libraryDependencies += "org.mindrot" % "jbcrypt" % "0.4" +libraryDependencies += "com.mailjet" % "mailjet-client" % "5.2.5" +libraryDependencies += "javax.mail" % "javax.mail-api" % "1.6.2" +libraryDependencies += "com.sun.mail" % "javax.mail" % "1.6.2" +libraryDependencies += "com.github.blemale" %% "scaffeine" % "5.2.1" +libraryDependencies += "net.java.dev.jna" % "jna" % "5.14.0" +libraryDependencies += "commons-codec" % "commons-codec" % "1.16.0" +libraryDependencies += "io.dropwizard.metrics" % "metrics-json" % "4.2.23" excludeAll (excludesJackson: _*) +libraryDependencies += "org.mozilla" % "rhino" % "1.7.14" +libraryDependencies += "com.squareup.okhttp3" % "okhttp" % "4.12.0" excludeAll (excludesJackson: _*) +libraryDependencies += "fr.maif" %% "wasm4s" % "3.0.0" classifier "bundle" +libraryDependencies += "com.auth0" % "java-jwt" % "4.4.0" excludeAll (excludesJackson: _*) // needed by wasm4s +libraryDependencies += "com.typesafe.akka" %% "akka-http" % "10.2.10" + +libraryDependencies += "org.scalatestplus.play" %% "scalatestplus-play" % "5.0.0" % Test +libraryDependencies += "org.scalatest" %% "scalatest-flatspec" % "3.2.12" % "test" +libraryDependencies += "com.github.tomakehurst" % "wiremock-jre8" % "2.34.0" % Test +libraryDependencies += "com.fasterxml.jackson.core" % "jackson-databind" % "2.13.4.2" % Test +libraryDependencies += "com.fasterxml.jackson.module" %% "jackson-module-scala" % "2.13.4" % Test +libraryDependencies += jdbc % "test" +libraryDependencies += "org.testcontainers" % "testcontainers" % "1.19.1" % Test +libraryDependencies += "fr.maif" %% "izanami-client" % "1.11.5" % Test +libraryDependencies += "org.awaitility" % "awaitility-scala" % "4.2.0" % Test +libraryDependencies += "com.github.mifmif" % "generex" % "1.0.1" % Test + +routesImport += "fr.maif.izanami.models.CustomBinders._" + +assembly / mainClass := Some("play.core.server.ProdServerStart") +assembly / fullClasspath += Attributed.blank(PlayKeys.playPackageAssets.value) +assembly / assemblyJarName := "izanami.jar" +assembly / assemblyMergeStrategy := { + case manifest if manifest.contains("MANIFEST.MF") => + // We don't need manifest files since sbt-assembly will create + // one with the given settings + MergeStrategy.discard + case referenceOverrides if referenceOverrides.contains("reference-overrides.conf") => + // Keep the content for all reference-overrides.conf files + MergeStrategy.concat + case PathList("reference.conf") => MergeStrategy.concat + case PathList(ps @ _*) if ps.last == "io.netty.versions.properties" => MergeStrategy.first + case referenceOverrides if referenceOverrides.contains("module-info.class") => MergeStrategy.discard + case referenceOverrides if referenceOverrides.contains("mailcap.default") => MergeStrategy.first + case PathList("javax", xs @ _*) => MergeStrategy.first + case PathList("javax", xs @ _*) => MergeStrategy.first + case referenceOverrides if referenceOverrides.startsWith("scala/") => MergeStrategy.first + case referenceOverrides if referenceOverrides.startsWith("play/") => MergeStrategy.first + case referenceOverrides if referenceOverrides.contains("library.properties") => MergeStrategy.first + case referenceOverrides if referenceOverrides.contains("mimetypes.default") => MergeStrategy.first + case PathList("META-INF", x, xs @ _*) if x.toLowerCase == "services" => MergeStrategy.filterDistinctLines + case PathList("META-INF", xs @ _*) => MergeStrategy.discard + + case x => + // For all the other files, use the default sbt-assembly merge strategy + val oldStrategy = (assembly / assemblyMergeStrategy).value + oldStrategy(x) +} + +releaseVersionBump := sbtrelease.Version.Bump.Bugfix +releaseProcess := Seq[ReleaseStep]( + checkSnapshotDependencies, // : ReleaseStep + inquireVersions, // : ReleaseStep + //runClean, // : ReleaseStep + //runTest, // : ReleaseStep + setReleaseVersion, // : ReleaseStep + commitReleaseVersion, // : ReleaseStep, performs the initial git checks + tagRelease, // : ReleaseStep + //publishArtifacts, // : ReleaseStep, checks whether `publishTo` is properly set up + setNextVersion // : ReleaseStep + //commitNextVersion, // : ReleaseStep + //pushChanges // : ReleaseStep, also checks that an upstream branch is properly configured +) + +// Adds additional packages into Twirl +//TwirlKeys.templateImports += "fr.maif.controllers._" + +// Adds additional packages into conf/routes +// play.sbt.routes.RoutesKeys.routesImport += "fr.maif.binders._" diff --git a/conf/application.conf b/conf/application.conf new file mode 100644 index 000000000..767f05438 --- /dev/null +++ b/conf/application.conf @@ -0,0 +1,310 @@ +# https://www.playframework.com/documentation/latest/Configuration +app { + secret = ${app.default-secret} + secret=${?IZANAMI_SECRET} + default-secret = "S_iVTvZcJhGxA^jPl2y9FLB/Yfh/OH3j/:ce>xa`wawr44Wufm_H3^u5ln7:tiDn" # default value + containerized="false" + containerized = ${?IZANAMI_CONTAINERIZED} + reporting { + url = "https://reporting.otoroshi.io/izanami/ingest" + url = ${?IZANAMI_REPORTING_URL} + } + wasm { + cache { + ttl = 60000 + ttl = ${?IZANAMI_WASM_CACHE_TTL} + } + } + admin { + username = "RESERVED_ADMIN_USER" + username = ${?IZANAMI_ADMIN_DEFAULT_USERNAME} + password = ${?IZANAMI_ADMIN_DEFAULT_PASSWORD} + } + exposition { + url= ${?IZANAMI_EXPOSITION_URL} + backend=${?app.exposition.url} + backend=${?IZANAMI_EXPOSITION_BACKEND} + } + openid { + client-id = ${?IZANAMI_OPENID_CLIENT_ID} + client-secret = ${?IZANAMI_OPENID_CLIENT_SECRET} + authorize-url = ${?IZANAMI_OPENID_AUTHORIZE_URL} + token-url = ${?IZANAMI_OPENID_TOKEN_URL} + redirect-url = ${?app.exposition.url}"/login" + redirect-url = ${?IZANAMI_OPENID_REDIRECT_URL} + } + wasmo { + url = ${?IZANAMI_WASMO_URL} + client-id = ${?IZANAMI_WASMO_CLIENT_ID} + client-secret = ${?IZANAMI_WASMO_CLIENT_SECRET} + } + pg { + uri = ${?IZANAMI_PG_URI} + uri = ${?POSTGRESQL_ADDON_URI} + pool-size = 20 + pool-size = ${?IZANAMI_PG_POOL_SIZE} + port = 5432 + port = ${?IZANAMI_PG_PORT} + port = ${?POSTGRESQL_ADDON_PORT} + //host = "localhost" + host = ${?IZANAMI_PG_HOST} + host = ${?POSTGRESQL_ADDON_HOST} + //database = "postgres" + database = ${?IZANAMI_PG_DATABASE} + database = ${?POSTGRESQL_ADDON_DB} + //user = "postgres" + user = ${?IZANAMI_PG_USER} + user = ${?POSTGRESQL_ADDON_USER} + //password = "postgres" + password = ${?IZANAMI_PG_PASSWORD} + password = ${?POSTGRESQL_ADDON_PASSWORD} + connect-timeout = ${?IZANAMI_PG_CONNECT_TIMEOUT} + idle-timeout = ${?IZANAMI_PG_IDLE_TIMEOUT} + log-activity = ${?IZANAMI_PG_LOG_ACTIVITY} + pipelining-limit = ${?IZANAMI_PG_PIPELINING_LIMIT} + ssl { + enabled = false + enabled = ${?IZANAMI_PG_SSL_ENABLED} + mode = "VERIFY_CA" + mode = ${?IZANAMI_PG_SSL_MODE} + trusted-certs-path = [] + trusted-certs = [] + trusted-cert-path = ${?IZANAMI_PG_SSL_TRUSTED_CERT_PATH} + trusted-cert = ${?IZANAMI_PG_SSL_TRUSTED_CERT} + client-certs-path = [] + client-certs = [] + client-cert-path = ${?IZANAMI_PG_SSL_CLIENT_CERT_PATH} + client-cert = ${?IZANAMI_PG_SSL_CLIENT_CERT} + trust-all = ${?IZANAMI_PG_SSL_TRUST_ALL} + } + } + authentication { + secret = ${?IZANAMI_TOKEN_SECRET} + secret = ${app.secret} + token-body-secret = ${?IZANAMI_TOKEN_BODY_SECRET} + token-body-secret = ${app.secret} + } + invitations { + ttl = ${?IZANAMI_INVITATIONS_TTL} + ttl = 86400 + } + sessions { + ttl = ${?IZANAMI_SESSIONS_TTL} + ttl = 3700 + } + password-reset-requests { + ttl = ${IZANAMI_PASSWORD_RESET_REQUEST_TTL} + ttl = 900 + } +} + +# Copyright (C) Lightbend Inc. + +# Configuration for Play's AkkaHttpServer +play { + filters { + cors { + pathPrefixes = ${?IZANAMI_CORS_PATH_PREFIXES} + allowedOrigins = ${?IZANAMI_CORS_ALLOWED_ORIGINS} + allowedHttpMethods = ${?IZANAMI_CORS_ALLOWED_HTTP_METHODS} + allowedHttpHeaders = ${?IZANAMI_CORS_ALLOWED_HTTP_HEADERS} + exposedHeaders = ${?IZANAMI_CORS_EXPOSED_HEADERS} + supportsCredentials = ${?IZANAMI_CORS_SUPPORTS_CREDENTIALS} + preflightMaxAge = ${?IZANAMI_CORS_PREFLIGHT_MAX_AGE} + serveForbiddenOrigins = ${?IZANAMI_CORS_SERVE_FORBIDDEN_ORIGINS} + } + } + server { + # The root directory for the Play server instance. This value can + # be set by providing a path as the first argument to the Play server + # launcher script. See `ServerConfig.loadConfiguration`. + dir = ${?user.dir} + + # HTTP configuration + http { + # The HTTP port of the server. Use a value of "disabled" if the server + # shouldn't bind an HTTP port. + port = 9000 + port = ${?PLAY_HTTP_PORT} + port = ${?http.port} + + # The interface address to bind to. + address = "0.0.0.0" + address = ${?PLAY_HTTP_ADDRESS} + address = ${?http.address} + + # The idle timeout for an open connection after which it will be closed + # Set to null or "infinite" to disable the timeout, but notice that this + # is not encouraged since timeout are important mechanisms to protect your + # servers from malicious attacks or programming mistakes. + idleTimeout = 75 seconds # The server provider class name + } + https { + + # The HTTPS port of the server. + port = ${?PLAY_HTTPS_PORT} + port = ${?https.port} + + # The interface address to bind to + address = "0.0.0.0" + address = ${?PLAY_HTTPS_ADDRESS} + address = ${?https.address} + + # The idle timeout for an open connection after which it will be closed + # Set to null or "infinite" to disable the timeout, but notice that this + # is not encouraged since timeout are important mechanisms to protect your + # servers from malicious attacks or programming mistakes. + idleTimeout = ${play.server.http.idleTimeout} + + # The SSL engine provider + engineProvider = "play.core.server.ssl.DefaultSSLEngineProvider" + engineProvider = ${?play.http.sslengineprovider} + + # HTTPS keystore configuration, used by the default SSL engine provider + keyStore { + # The path to the keystore + path = ${?https.keyStore} + + # The type of the keystore + type = "JKS" + type = ${?https.keyStoreType} + + # The password for the keystore + password = "" + password = ${?https.keyStorePassword} + + # The algorithm to use. If not set, uses the platform default algorithm. + algorithm = ${?https.keyStoreAlgorithm} + } + + # HTTPS truststore configuration + trustStore { + + # If true, does not do CA verification on client side certificates + noCaVerification = false + } + + # Whether JSSE want client auth mode should be used. This means, the server + # will request a client certificate, but won't fail if one isn't provided. + wantClientAuth = false + + # Whether JSSE need client auth mode should be used. This means, the server + # will request a client certificate, and will fail and terminate the session + # if one isn't provided. + needClientAuth = false + } + # The path to the process id file created by the server when it runs. + # If set to "/dev/null" then no pid file will be created. + pidfile.path = ${play.server.dir}/RUNNING_PID + pidfile.path = ${?pidfile.path} + + websocket { + # Maximum allowable frame payload length. Setting this value to your application's + # requirement may reduce denial of service attacks using long data frames. + frame.maxLength = 64k + frame.maxLength = ${?websocket.frame.maxLength} + + # Periodic keep alive may be implemented using by sending Ping frames + # upon which the other side is expected to reply with a Pong frame, + # or by sending a Pong frame, which serves as unidirectional heartbeat. + # Valid values: + # ping - default, for bi-directional ping/pong keep-alive heartbeating + # pong - for uni-directional pong keep-alive heartbeating + periodic-keep-alive-mode = ping + + # Interval for sending periodic keep-alives + # If a client does not send a frame within this idle time, the server will sent the the keep-alive frame. + # The frame sent will be the one configured in play.server.websocket.periodic-keep-alive-mode + # `infinite` by default, or a duration that is the max idle interval after which an keep-alive frame should be sent + # The value `infinite` means that *no* keep-alive heartbeat will be sent, as: "the allowed idle time is infinite" + periodic-keep-alive-max-idle = infinite + } + + debug { + # If set to true this will attach an attribute to each request containing debug information. If the application + # fails to load (e.g. due to a compile issue in dev mode), then this configuration value is ignored and the debug + # information is always attached. + # + # Note: This configuration option is not part of Play's public API and is subject to change without the usual + # deprecation cycle. + addDebugInfoToRequests = false + } + + # The maximum length of the HTTP headers. The most common effect of this is a restriction in cookie length, including + # number of cookies and size of cookie values. + max-header-size = 8k + + # If a request contains a Content-Length header it will be checked against this maximum value. + # If the value of a given Content-Length header exceeds this configured value, the request will not be processed + # further but instead the error handler will be called with Http status code 413 "Entity too large". + # If set to infinite or if no Content-Length header exists then no check will take place at all + # and the request will continue to be processed. + # Play uses the concept of a `BodyParser` to enforce this limit, so we set it to infinite. + max-content-length = infinite + } + + editor = ${?PLAY_EDITOR} + provider = "play.core.server.AkkaHttpServerProvider" + akka { + # How long to wait when binding to the listening socket + bindTimeout = 5 seconds + + # How long a request takes until it times out. Set to null or "infinite" to disable the timeout. + requestTimeout = infinite + + # Timeout after which all requests and connections shall be forcefully terminated + # when shutting down the server. It will default to Coordinated Shutdown service-unbind + # phase timeout. Value must be a duration, for example: + # play.server.akka.terminationTimeout = 10 seconds + terminationTimeout = null + + # Enables/disables automatic handling of HEAD requests. + # If this setting is enabled the server dispatches HEAD requests as GET + # requests to the application and automatically strips off all message + # bodies from outgoing responses. + # Note that, even when this setting is off the server will never send + # out message bodies on responses to HEAD requests. + transparent-head-requests = off + + # If this setting is empty the server only accepts requests that carry a + # non-empty `Host` header. Otherwise it responds with `400 Bad Request`. + # Set to a non-empty value to be used in lieu of a missing or empty `Host` + # header to make the server accept such requests. + # Note that the server will never accept HTTP/1.1 request without a `Host` + # header, i.e. this setting only affects HTTP/1.1 requests with an empty + # `Host` header as well as HTTP/1.0 requests. + # Examples: `www.spray.io` or `example.com:8080` + default-host-header = "" + + # The default value of the `Server` header to produce if no + # explicit `Server`-header was included in a response. + # If this value is null and no header was included in + # the request, no `Server` header will be rendered at all. + server-header = null + server-header = ${?play.server.server-header} + + # Configures the processing mode when encountering illegal characters in + # header value of response. + # + # Supported mode: + # `error` : default mode, throw an ParsingException and terminate the processing + # `warn` : ignore the illegal characters in response header value and log a warning message + # `ignore` : just ignore the illegal characters in response header value + illegal-response-header-value-processing-mode = warn + + # Enables/disables inclusion of an Tls-Session-Info header in parsed + # messages over Tls transports (i.e., HttpRequest on server side and + # HttpResponse on client side). + # + # See Akka HTTP `akka.http.server.parsing.tls-session-info-header` for + # more information about how this works. + tls-session-info-header = on + } +} +play.http.secret.key = ${app.secret} +play.application.loader = fr.maif.izanami.IzanamiLoader +play.http.errorHandler = play.api.http.JsonHttpErrorHandler +# This must be big to accept base64 wasm features +play.http.parser.maxMemoryBuffer=4MB +play.assets.cache."/public/index.html"="no-cache, no-store, must-revalidate" +play.assets.cache."/public/assets/"="max-age=30240000" \ No newline at end of file diff --git a/conf/dev.conf b/conf/dev.conf new file mode 100644 index 000000000..2367bfcff --- /dev/null +++ b/conf/dev.conf @@ -0,0 +1,37 @@ +include "application.conf" + +app { + secret = "S_iVTvZcJhGxA^jPl2y9FLB/Yfh/OH3j/:ce>xa`wawr44Wufm_H3^u5ln7:tiDn" + config { + mode = "dev" + } + pg { + port = 5432 + host = "localhost" + database = "postgres" + user = "postgres" + password = "postgres" + } + openid { + client-id = foo + client-secret = bar + authorize-url = "http://localhost:9001/auth" + token-url = "http://localhost:9001/token" + redirect-url = "http://localhost:3000/login" + } + admin { + password = "ADMIN_DEFAULT_PASSWORD" + } + exposition { + url= "http://localhost:3000" + backend= "http://localhost:9000" + } + wasmo { + url="http://localhost:5001" + client-id="admin-api-apikey-id" + client-secret="admin-api-apikey-secret" + } +} + +play.application.loader = "fr.maif.izanami.IzanamiLoader" +play.server.dir = "." \ No newline at end of file diff --git a/conf/logback.xml b/conf/logback.xml new file mode 100644 index 000000000..01f80ac05 --- /dev/null +++ b/conf/logback.xml @@ -0,0 +1,44 @@ + + + + + + + ${application.home:-.}/logs/application.log + + UTF-8 + + %d{yyyy-MM-dd HH:mm:ss} %highlight(%-5level) %cyan(%logger{36}) %magenta(%X{akkaSource}) %msg%n + + + + + + + UTF-8 + + %d{yyyy-MM-dd HH:mm:ss} %highlight(%-5level) %cyan(%logger{36}) %magenta(%X{akkaSource}) %msg%n + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/conf/messages b/conf/messages new file mode 100644 index 000000000..0226738a6 --- /dev/null +++ b/conf/messages @@ -0,0 +1 @@ +# https://www.playframework.com/documentation/latest/ScalaI18N diff --git a/conf/routes b/conf/routes new file mode 100644 index 000000000..b9d7eecda --- /dev/null +++ b/conf/routes @@ -0,0 +1,121 @@ +# Routes +# This file defines all application routes (Higher priority routes first) +# https://www.playframework.com/documentation/latest/ScalaRouting +# ~~~~ + +# Administration endpoints +POST /api/admin/tenants fr.maif.izanami.web.TenantController.createTenant() +PUT /api/admin/tenants/:name fr.maif.izanami.web.TenantController.updateTenant(name: String) +GET /api/admin/tenants fr.maif.izanami.web.TenantController.readTenants(right: Option[fr.maif.izanami.models.RightLevels.RightLevel]) +GET /api/admin/tenants/:name fr.maif.izanami.web.TenantController.readTenant(name: String) +DELETE /api/admin/tenants/:name fr.maif.izanami.web.TenantController.deleteTenant(name: String) + +POST /api/admin/tenants/:tenant/contexts/*parents fr.maif.izanami.web.FeatureContextController.createGlobalSubContext(tenant: String, parents: fr.maif.izanami.web.FeatureContextPath) +POST /api/admin/tenants/:tenant/contexts fr.maif.izanami.web.FeatureContextController.createGlobalRootSubContext(tenant: String) +GET /api/admin/tenants/:tenant/contexts fr.maif.izanami.web.FeatureContextController.readGlobalContexts(tenant: String, all: Boolean ?= false) +DELETE /api/admin/tenants/:tenant/contexts/*context fr.maif.izanami.web.FeatureContextController.deleteGlobalFeatureContext(tenant: String, context: fr.maif.izanami.web.FeatureContextPath) + +POST /api/admin/tenants/:tenant/projects fr.maif.izanami.web.ProjectController.createProject(tenant: String) +PUT /api/admin/tenants/:tenant/projects/:project fr.maif.izanami.web.ProjectController.updateProject(tenant: String, project: String) +GET /api/admin/tenants/:tenant/projects fr.maif.izanami.web.ProjectController.readProjects(tenant: String) +GET /api/admin/tenants/:tenant/projects/:project fr.maif.izanami.web.ProjectController.readProject(tenant: String, project: String) +DELETE /api/admin/tenants/:tenant/projects/:project fr.maif.izanami.web.ProjectController.deleteProject(tenant: String, project: String) + +POST /api/admin/tenants/:tenant/projects/:project/features fr.maif.izanami.web.FeatureController.createFeature(tenant: String, project: String) +PUT /api/admin/tenants/:tenant/features/:id fr.maif.izanami.web.FeatureController.updateFeature(tenant: String, id: String) +DELETE /api/admin/tenants/:tenant/features/:id fr.maif.izanami.web.FeatureController.deleteFeature(tenant: String, id: String) +GET /api/admin/tenants/:tenant/features fr.maif.izanami.web.FeatureController.searchFeatures(tenant: String, tag: String ?= "") +PATCH /api/admin/tenants/:tenant/features fr.maif.izanami.web.FeatureController.patchFeatures(tenant: String) +GET /api/admin/tenants/:tenant/features/_test fr.maif.izanami.web.FeatureController.testFeaturesForContext(tenant: String, user: String ?= "", date: Option[java.time.Instant], featureRequest: fr.maif.izanami.models.FeatureRequest) +POST /api/admin/tenants/:tenant/test fr.maif.izanami.web.FeatureController.testFeature(tenant: String, user: String ?= "", date: java.time.Instant) +GET /api/admin/tenants/:tenant/features/:id/test/*context fr.maif.izanami.web.FeatureController.testExistingFeature(tenant: String, context: fr.maif.izanami.web.FeatureContextPath, id: String, user: String ?= "", date: java.time.Instant) +GET /api/admin/tenants/:tenant/features/:id/test fr.maif.izanami.web.FeatureController.testExistingFeatureWithoutContext(tenant: String, id: String, user: String ?= "", date: java.time.Instant) + + +POST /api/admin/tenants/:tenant/tags fr.maif.izanami.web.TagController.createTag(tenant: String) +GET /api/admin/tenants/:tenant/tags/:name fr.maif.izanami.web.TagController.readTag(tenant: String, name: String) +DELETE /api/admin/tenants/:tenant/tags/:name fr.maif.izanami.web.TagController.deleteTag(tenant: String, name: String) +GET /api/admin/tenants/:tenant/tags fr.maif.izanami.web.TagController.readTags(tenant: String) + +POST /api/admin/tenants/:tenant/keys fr.maif.izanami.web.ApiKeyController.createApiKey(tenant: String) +GET /api/admin/tenants/:tenant/keys fr.maif.izanami.web.ApiKeyController.readApiKey(tenant: String) +DELETE /api/admin/tenants/:tenant/keys/:name fr.maif.izanami.web.ApiKeyController.deleteApiKey(tenant: String, name: String) +PUT /api/admin/tenants/:tenant/keys/:name fr.maif.izanami.web.ApiKeyController.updateApiKey(tenant: String, name: String) + + +POST /api/admin/tenants/:tenant/projects/:project/contexts fr.maif.izanami.web.FeatureContextController.createFeatureContext(tenant: String, project: String) +PUT /api/admin/tenants/:tenant/projects/:project/contexts/*parents/features/:name fr.maif.izanami.web.FeatureContextController.createFeatureStrategy(tenant: String, project: String, parents: fr.maif.izanami.web.FeatureContextPath, name: String) +DELETE /api/admin/tenants/:tenant/projects/:project/contexts/*parents/features/:name fr.maif.izanami.web.FeatureContextController.deleteFeatureStrategy(tenant: String, project: String, parents: fr.maif.izanami.web.FeatureContextPath, name: String) +POST /api/admin/tenants/:tenant/projects/:project/contexts/*parents fr.maif.izanami.web.FeatureContextController.createSubContext(tenant: String, project: String, parents: fr.maif.izanami.web.FeatureContextPath) +DELETE /api/admin/tenants/:tenant/projects/:project/contexts/*context fr.maif.izanami.web.FeatureContextController.deleteFeatureContext(tenant: String, project: String, context: fr.maif.izanami.web.FeatureContextPath) + +GET /api/admin/tenants/:tenant/projects/:project/contexts fr.maif.izanami.web.FeatureContextController.readFeatureContexts(tenant: String, project: String) + +GET /api/admin/users/search fr.maif.izanami.web.UserController.searchUsers(query: String, count: Integer) +POST /api/admin/tenants/:tenant/projects/:project/users fr.maif.izanami.web.UserController.inviteUsersToProject(tenant: String, project: String) +POST /api/admin/tenants/:tenant/users fr.maif.izanami.web.UserController.inviteUsersToTenant(tenant: String) +GET /api/admin/users/rights fr.maif.izanami.web.UserController.readRights() +POST /api/admin/password/_reset fr.maif.izanami.web.UserController.resetPassword() +POST /api/admin/password/_reinitialize fr.maif.izanami.web.UserController.reinitializePassword() +DELETE /api/admin/users/:user fr.maif.izanami.web.UserController.deleteUser(user: String) +GET /api/admin/users/:user fr.maif.izanami.web.UserController.readUser(user: String) +GET /api/admin/tenants/:tenant/users fr.maif.izanami.web.UserController.readUsersForTenant(tenant: String) +GET /api/admin/tenants/:tenant/projects/:project/users fr.maif.izanami.web.UserController.readUsersForProject(tenant: String, project: String) +PUT /api/admin/tenants/:tenant/projects/:project/users/:user/rights fr.maif.izanami.web.UserController.updateUserRightsForProject(tenant: String, project: String, user: String) +GET /api/admin/:tenant/users/:user fr.maif.izanami.web.UserController.readUserForTenant(tenant: String, user: String) +PUT /api/admin/:tenant/users/:user/rights fr.maif.izanami.web.UserController.updateUserRightsForTenant(tenant: String, user: String) +PUT /api/admin/users/:user/rights fr.maif.izanami.web.UserController.updateUserRights(user: String) +PUT /api/admin/users/:user/password fr.maif.izanami.web.UserController.updateUserPassword(user: String) +PUT /api/admin/users/:user fr.maif.izanami.web.UserController.updateUser(user: String) +GET /api/admin/users fr.maif.izanami.web.UserController.readUsers() +POST /api/admin/users fr.maif.izanami.web.UserController.createUser() +POST /api/admin/invitation fr.maif.izanami.web.UserController.sendInvitation() +POST /api/admin/login fr.maif.izanami.web.LoginController.login(rights: Boolean ?= false) +POST /api/admin/logout fr.maif.izanami.web.LoginController.logout() +GET /api/admin/openid-connect fr.maif.izanami.web.LoginController.openIdConnect() +POST /api/admin/openid-connect-callback fr.maif.izanami.web.LoginController.openIdCodeReturn() + +PUT /api/admin/configuration fr.maif.izanami.web.ConfigurationController.updateConfiguration() +GET /api/admin/configuration fr.maif.izanami.web.ConfigurationController.readConfiguration() +GET /api/admin/exposition fr.maif.izanami.web.ConfigurationController.readExpositionUrl() +GET /api/admin/integrations fr.maif.izanami.web.ConfigurationController.availableIntegrations() +GET /api/admin/configuration/mailer/:id fr.maif.izanami.web.ConfigurationController.readMailerConfiguration(id: String) +PUT /api/admin/configuration/mailer/:id fr.maif.izanami.web.ConfigurationController.updateMailerConfiguration(id: String) + +GET /api/admin/plugins fr.maif.izanami.web.PluginController.wasmFiles() +GET /api/admin/tenants/:tenant/local-scripts fr.maif.izanami.web.PluginController.localScripts(tenant: String, features: Boolean ?= false) +PUT /api/admin/tenants/:tenant/local-scripts/:script fr.maif.izanami.web.PluginController.updateScript(tenant: String, script: String) +DELETE /api/admin/local-scripts/_cache fr.maif.izanami.web.PluginController.clearWasmCache() +DELETE /api/admin/tenants/:tenant/local-scripts/:script fr.maif.izanami.web.PluginController.deleteScript(tenant: String, script: String) + +POST /api/admin/tenants/:tenant/_import fr.maif.izanami.web.ImportController.importData(tenant: String, conflict: String, timezone: String, deduceProject: Boolean, create: Option[Boolean], project: Option[String], projectPartSize: Option[Int], inlineScript: Option[Boolean]) +GET /api/admin/tenants/:tenant/_import/:id fr.maif.izanami.web.ImportController.readImportStatus(tenant: String, id: String) +DELETE /api/admin/tenants/:tenant/_import/:id fr.maif.izanami.web.ImportController.deleteImportStatus(tenant: String, id: String) + +GET /api/admin/stats fr.maif.izanami.web.ConfigurationController.readStats() + +# Client application endpoints +GET /api/v2/features/:id fr.maif.izanami.web.FeatureController.checkFeatureForContext(id: String, user: String ?= "", context: fr.maif.izanami.web.FeatureContextPath) +POST /api/v2/features/:id fr.maif.izanami.web.FeatureController.checkFeatureForContext(id: String, user: String ?= "", context: fr.maif.izanami.web.FeatureContextPath) +GET /api/v2/features fr.maif.izanami.web.FeatureController.evaluateFeaturesForContext(user: String ?= "", conditions: Boolean ?= false, date: Option[java.time.Instant], featureRequest: fr.maif.izanami.models.FeatureRequest) +POST /api/v2/features fr.maif.izanami.web.FeatureController.evaluateFeaturesForContext(user: String ?= "", conditions: Boolean ?= false, date: Option[java.time.Instant], featureRequest: fr.maif.izanami.models.FeatureRequest) + +# V1 compatibility endpoints + +GET /api/features/:pattern/check fr.maif.izanami.web.LegacyController.legacyFeature(pattern: String) +POST /api/features/:pattern/check fr.maif.izanami.web.LegacyController.legacyFeature(pattern: String) +GET /api/features fr.maif.izanami.web.LegacyController.legacyFeatures(pattern: String, active: Boolean, page: Int, pageSize: Int) +GET /api/events fr.maif.izanami.web.EventController.events(pattern: String ?= "*", domains: String ?= "") +GET /api/_health fr.maif.izanami.web.LegacyController.healthcheck() + +# Frontend endpoint +# Serve index page from public directory +GET / fr.maif.izanami.web.FrontendController.index() +# Serve static assets under public directory +GET /*file fr.maif.izanami.web.FrontendController.assetOrDefault(file) +#GET / controllers.Assets.at(path="/public", file="index.html") +#GET /*file controllers.Assets.versioned(path="/public", file: Asset) +#GET /*route controllers.Assets.at(path="/public", file="index.html") + +#OPTIONS / fr.maif.izanami.web.FrontendController.rootOptions +#OPTIONS /*url fr.maif.izanami.web.FrontendController.options(url: String) \ No newline at end of file diff --git a/conf/sql/globals/V10__create_current_import_table.sql b/conf/sql/globals/V10__create_current_import_table.sql new file mode 100644 index 000000000..68e898bf9 --- /dev/null +++ b/conf/sql/globals/V10__create_current_import_table.sql @@ -0,0 +1,8 @@ +CREATE TYPE IMPORT_STATUS AS ENUM ('PENDING', 'FINISHED', 'FAILED'); + +CREATE TABLE pending_imports ( + id UUID DEFAULT gen_random_uuid () PRIMARY KEY, + status IMPORT_STATUS NOT NULL DEFAULT 'PENDING', + creation TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(), + result JSON +) \ No newline at end of file diff --git a/conf/sql/globals/V1__create_tenant_table.sql b/conf/sql/globals/V1__create_tenant_table.sql new file mode 100644 index 000000000..8ade42d30 --- /dev/null +++ b/conf/sql/globals/V1__create_tenant_table.sql @@ -0,0 +1,4 @@ +CREATE TABLE tenants ( + name TEXT PRIMARY KEY, + description TEXT NOT NULL DEFAULT '' +); \ No newline at end of file diff --git a/conf/sql/globals/V2__create_user_table.sql b/conf/sql/globals/V2__create_user_table.sql new file mode 100644 index 000000000..179b765eb --- /dev/null +++ b/conf/sql/globals/V2__create_user_table.sql @@ -0,0 +1,14 @@ +CREATE TYPE USER_TYPE AS ENUM ('INTERNAL', 'OIDC', 'OTOROSHI'); + +CREATE TABLE users ( + username TEXT NOT NULL PRIMARY KEY, + password TEXT, + email TEXT UNIQUE, + admin BOOLEAN NOT NULL DEFAULT false, + user_type USER_TYPE NOT NULL DEFAULT 'INTERNAL', + default_tenant TEXT DEFAULT NULL REFERENCES tenants(name) ON DELETE SET NULL ON UPDATE CASCADE, + legacy BOOLEAN NOT NULL DEFAULT false, + CHECK ((user_type='OTOROSHI' OR user_type='OIDC') OR (user_type='INTERNAL' AND password IS NOT NULL AND email IS NOT NULL)) +); + +INSERT INTO users(email, username, password, admin) VALUES ('foo.bar@somemail.com', '${default_admin}', '${default_password}', true); \ No newline at end of file diff --git a/conf/sql/globals/V3__create_user_tenant_right_table.sql b/conf/sql/globals/V3__create_user_tenant_right_table.sql new file mode 100644 index 000000000..8171fcd93 --- /dev/null +++ b/conf/sql/globals/V3__create_user_tenant_right_table.sql @@ -0,0 +1,8 @@ +CREATE TYPE RIGHT_LEVEL AS ENUM ('READ', 'WRITE', 'ADMIN'); + +CREATE TABLE users_tenants_rights ( + username TEXT NOT NULL REFERENCES users(username) ON DELETE CASCADE ON UPDATE CASCADE, + tenant TEXT NOT NULL REFERENCES tenants(name) ON DELETE CASCADE ON UPDATE CASCADE, + level RIGHT_LEVEL NOT NULL DEFAULT 'READ', + PRIMARY KEY (username, tenant) +); diff --git a/conf/sql/globals/V4__create_mailer_table.sql b/conf/sql/globals/V4__create_mailer_table.sql new file mode 100644 index 000000000..54490e535 --- /dev/null +++ b/conf/sql/globals/V4__create_mailer_table.sql @@ -0,0 +1,9 @@ +CREATE TABLE MAILERS( + name TEXT PRIMARY KEY, + configuration JSONB NOT NULL DEFAULT '{}' +); + +INSERT INTO MAILERS(name) VALUES ('CONSOLE'); +INSERT INTO MAILERS(name) VALUES ('MAILJET'); +INSERT INTO MAILERS(name) VALUES ('MAILGUN'); +INSERT INTO MAILERS(name) VALUES ('SMTP'); \ No newline at end of file diff --git a/conf/sql/globals/V5__create_configuration_table.sql b/conf/sql/globals/V5__create_configuration_table.sql new file mode 100644 index 000000000..587780c97 --- /dev/null +++ b/conf/sql/globals/V5__create_configuration_table.sql @@ -0,0 +1,13 @@ +CREATE TABLE configuration ( + onerow_id bool PRIMARY KEY DEFAULT TRUE, + mailer TEXT REFERENCES mailers(name) NOT NULL, + invitation_mode TEXT NOT NULL, + origin_email TEXT, + izanami_id UUID NOT NULL DEFAULT gen_random_uuid(), + anonymous_reporting BOOLEAN DEFAULT false, + anonymous_reporting_date TIMESTAMP WITH TIME ZONE, + CONSTRAINT onerow_uni CHECK (onerow_id) +); + +INSERT INTO configuration(mailer, invitation_mode) +values ('CONSOLE', 'RESPONSE'); \ No newline at end of file diff --git a/conf/sql/globals/V6__create_invitation_table.sql b/conf/sql/globals/V6__create_invitation_table.sql new file mode 100644 index 000000000..f60784438 --- /dev/null +++ b/conf/sql/globals/V6__create_invitation_table.sql @@ -0,0 +1,8 @@ +CREATE TABLE invitations ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid (), + email TEXT UNIQUE NOT NULL, + creation TIMESTAMP WITH TIME ZONE DEFAULT NOW(), + admin BOOLEAN NOT NULL DEFAULT false, + rights JSONB NOT NULL DEFAULT '{}'::jsonb, + inviter TEXT REFERENCES users (username) ON DELETE CASCADE ON UPDATE CASCADE +); \ No newline at end of file diff --git a/conf/sql/globals/V7__create_session_table.sql b/conf/sql/globals/V7__create_session_table.sql new file mode 100644 index 000000000..ad001528c --- /dev/null +++ b/conf/sql/globals/V7__create_session_table.sql @@ -0,0 +1,5 @@ +CREATE TABLE sessions ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid (), + username TEXT NOT NULL REFERENCES users(username) ON DELETE CASCADE ON UPDATE CASCADE, + creation TIMESTAMP WITH TIME ZONE DEFAULT NOW() +); \ No newline at end of file diff --git a/conf/sql/globals/V8__create_password_reset_request_table.sql b/conf/sql/globals/V8__create_password_reset_request_table.sql new file mode 100644 index 000000000..6e557fbf8 --- /dev/null +++ b/conf/sql/globals/V8__create_password_reset_request_table.sql @@ -0,0 +1,5 @@ +CREATE TABLE password_reset ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid (), + creation TIMESTAMP WITH TIME ZONE DEFAULT NOW(), + username TEXT UNIQUE NOT NULL REFERENCES users(username) ON DELETE CASCADE ON UPDATE CASCADE +); \ No newline at end of file diff --git a/conf/sql/globals/V9__create_key_tenant_table.sql b/conf/sql/globals/V9__create_key_tenant_table.sql new file mode 100644 index 000000000..89aeb19cf --- /dev/null +++ b/conf/sql/globals/V9__create_key_tenant_table.sql @@ -0,0 +1,5 @@ +CREATE TABLE key_tenant ( + client_id TEXT, + tenant TEXT REFERENCES tenants(name) ON DELETE CASCADE ON UPDATE CASCADE, + PRIMARY KEY (client_id, tenant) +) \ No newline at end of file diff --git a/conf/sql/tenants/V0__create_tenant_schema.sql b/conf/sql/tenants/V0__create_tenant_schema.sql new file mode 100644 index 000000000..934ec6d89 --- /dev/null +++ b/conf/sql/tenants/V0__create_tenant_schema.sql @@ -0,0 +1,111 @@ +CREATE TABLE projects ( + id UUID DEFAULT gen_random_uuid () PRIMARY KEY, + name TEXT NOT NULL UNIQUE, + description TEXT NOT NULL DEFAULT '' +); + +CREATE TABLE wasm_script_configurations ( + id TEXT PRIMARY KEY, + config JSONB NOT NULL +); + +CREATE TABLE features ( + id TEXT DEFAULT (gen_random_uuid ())::TEXT PRIMARY KEY, + name TEXT NOT NULL, + project TEXT NOT NULL REFERENCES projects (name) ON DELETE CASCADE ON UPDATE CASCADE, + enabled BOOLEAN NOT NULL, + conditions JSONB, + script_config TEXT REFERENCES wasm_script_configurations(id) ON DELETE RESTRICT ON UPDATE CASCADE, + metadata JSONB NOT NULL DEFAULT '{}', + description TEXT NOT NULL DEFAULT '', + CONSTRAINT unique_feature_name_for_project UNIQUE (name, project), + CONSTRAINT feature_type_xor CHECK ((conditions is not null and script_config is null) or (conditions is null and script_config is not null)) +); + +CREATE INDEX features_project ON features (project); + + +CREATE TABLE tags ( + id UUID DEFAULT gen_random_uuid () PRIMARY KEY, + name TEXT UNIQUE, + description TEXT +); + +CREATE TABLE features_tags ( + tag TEXT NOT NULL REFERENCES tags (name) ON DELETE CASCADE ON UPDATE CASCADE, + feature TEXT NOT NULL REFERENCES features (id) ON DELETE CASCADE ON UPDATE CASCADE, + PRIMARY KEY (tag, feature) +); + +CREATE TABLE apikeys ( + clientid TEXT UNIQUE NOT NULL, + name TEXT PRIMARY KEY, + clientsecret TEXT NOT NULL, + description TEXT NOT NULL DEFAULT '', + enabled BOOLEAN NOT NULL DEFAULT true, + legacy BOOLEAN NOT NULL DEFAULT false, + admin BOOLEAN NOT NULL DEFAULT false +); + +CREATE INDEX apikeys_clientid ON apikeys (clientid); +CREATE INDEX apikeys_clientsecret ON apikeys (clientsecret); + + +CREATE TABLE apikeys_projects ( + apikey TEXT REFERENCES apikeys (name) ON DELETE CASCADE ON UPDATE CASCADE, + project TEXT REFERENCES projects(name) ON DELETE CASCADE ON UPDATE CASCADE, + PRIMARY KEY (apikey, project) +); + +CREATE TABLE global_feature_contexts ( + id TEXT PRIMARY KEY, + name TEXT NOT NULL, + parent TEXT REFERENCES global_feature_contexts(id) ON DELETE CASCADE ON UPDATE CASCADE +); + +CREATE TABLE feature_contexts ( + id TEXT PRIMARY KEY, + name TEXT NOT NULL, + parent TEXT REFERENCES feature_contexts(id) ON DELETE CASCADE ON UPDATE CASCADE, + global_parent TEXT REFERENCES global_feature_contexts(id) ON DELETE CASCADE ON UPDATE CASCADE, + project TEXT NOT NULL REFERENCES projects(name) ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT feature_contexts_xor CHECK ((global_parent is not null or parent is not null) or (parent is null and global_parent is null)) +); + +CREATE TABLE feature_contexts_strategies ( + project TEXT NOT NULL REFERENCES projects(name) ON DELETE CASCADE ON UPDATE CASCADE, + local_context TEXT REFERENCES feature_contexts(id) ON DELETE CASCADE ON UPDATE CASCADE, + global_context TEXT REFERENCES global_feature_contexts(id) ON DELETE CASCADE ON UPDATE CASCADE, + context TEXT GENERATED ALWAYS AS (case when local_context is null then global_context else local_context end) STORED, + context_path TEXT GENERATED ALWAYS AS (case when local_context is null then SUBSTRING(global_context FROM POSITION('_' IN global_context)+1) else SUBSTRING(local_context FROM POSITION('_' IN local_context)+1) end) STORED, + feature TEXT NOT NULL, + conditions JSONB, + script_config TEXT REFERENCES wasm_script_configurations(id) ON DELETE CASCADE ON UPDATE CASCADE, + enabled BOOLEAN NOT NULL, + PRIMARY KEY (project, context, feature), + FOREIGN KEY(feature, project) REFERENCES features(name, project) ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT feature_context_type_xor CHECK ((conditions is not null and script_config is null) or (conditions is null and script_config is not null)), + CONSTRAINT feature_context_type_xor_context CHECK ((local_context is not null and global_context is null) or (local_context is null and global_context is not null)) +); + +-- This table is used to store both global subcontext AND local context inheriting from global subcontext +-- in order to avoid having both a global and local subcontext with the same name for the same global context +CREATE TABLE feature_context_name_unicity_check_table ( + parent TEXT, + context TEXT, + PRIMARY KEY (parent, context) +); + +CREATE TABLE users_projects_rights ( + username TEXT NOT NULL REFERENCES izanami.users(username) ON DELETE CASCADE ON UPDATE CASCADE, + project TEXT NOT NULL REFERENCES projects(name) ON DELETE CASCADE ON UPDATE CASCADE, + level izanami.RIGHT_LEVEL NOT NULL DEFAULT 'READ', + PRIMARY KEY (username, project) +); + +CREATE TABLE users_keys_rights ( + username TEXT NOT NULL REFERENCES izanami.users(username) ON DELETE CASCADE ON UPDATE CASCADE, + apikey TEXT NOT NULL REFERENCES apikeys(name) ON DELETE CASCADE ON UPDATE CASCADE, + level izanami.RIGHT_LEVEL NOT NULL DEFAULT 'READ', + PRIMARY KEY (username, apikey) +); diff --git a/default-oidc-users.json b/default-oidc-users.json new file mode 100644 index 000000000..b416e8205 --- /dev/null +++ b/default-oidc-users.json @@ -0,0 +1,123 @@ +[ + { + "id": "SIMPLE_OIDC_USER_HARLEY", + "email": "harley@qlik.example", + "email_verified": true, + "name": "Harley Kiffe", + "nickname": "harley", + "password": "Password1!", + "groups": [ + "Everyone", + "Sales" + ] + }, + { + "id": "SIMPLE_OIDC_USER_BARB", + "email": "barb@qlik.example", + "email_verified": true, + "name": "Barb Stovin", + "nickname": "barb", + "password": "Password1!", + "groups": [ + "Everyone", + "Support" + ] + }, + { + "id": "SIMPLE_OIDC_USER_QUINN", + "email": "quinn@qlik.example", + "email_verified": true, + "name": "Quinn Leeming", + "nickname": "quinn", + "password": "Password1!", + "groups": [ + "Everyone", + "Accounting" + ] + }, + { + "id": "SIMPLE_OIDC_USER_SIM", + "email": "sim@qlik.example", + "email_verified": true, + "name": "Sim Cleaton", + "nickname": "sim", + "password": "Password1!", + "groups": [ + "Everyone", + "Accounting" + ] + }, + { + "id": "SIMPLE_OIDC_USER_PHILLIE", + "email": "phillie@qlik.example", + "email_verified": true, + "name": "Phillie Smeed", + "nickname": "phillie", + "password": "Password1!", + "groups": [ + "Everyone", + "Marketing", + "Sales" + ] + }, + { + "id": "SIMPLE_OIDC_USER_PETA", + "email": "peta@qlik.example", + "email_verified": true, + "name": "Peta Sammon", + "nickname": "peta", + "password": "Password1!", + "groups": [ + "Everyone", + "Engineering" + ] + }, + { + "id": "SIMPLE_OIDC_USER_MARNE", + "email": "marne@qlik.example", + "email_verified": true, + "name": "Marne Probetts", + "nickname": "marne", + "password": "Password1!", + "groups": [ + "Everyone", + "Marketing" + ] + }, + { + "id": "SIMPLE_OIDC_USER_SIBYLLA", + "email": "sibylla@qlik.example", + "email_verified": true, + "name": "Sibylla Meadows", + "nickname": "sibylla", + "password": "Password1!", + "groups": [ + "Everyone", + "Accounting" + ] + }, + { + "id": "SIMPLE_OIDC_USER_EVAN", + "email": "evan@qlik.example", + "email_verified": true, + "name": "Evan Highman", + "nickname": "evan", + "password": "Password1!", + "groups": [ + "Everyone", + "Engineering" + ] + }, + { + "id": "SIMPLE_OIDC_USER_FRANKLIN", + "email": "franklin@qlik.example", + "email_verified": true, + "name": "Franklin Glamart", + "nickname": "franklin", + "password": "Password1!", + "groups": [ + "Everyone", + "Sales" + ] + } +] \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 000000000..a18f02fec --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,88 @@ +# Use postgres/example user/password credentials +version: '3.1' + +services: + db: + image: postgres + restart: always + environment: + POSTGRES_USER: postgres + POSTGRES_PASSWORD: postgres +# volumes: +# - ./schema.sql:/docker-entrypoint-initdb.d/schema.sql + ports: + - 5432:5432 + openid: + image: qlik/simple-oidc-provider + environment: + REDIRECTS: http://localhost:3000/login + ports: + - 9001:9000 + fake-smtp: + image: reachfive/fake-smtp-server + ports: + - 1081:1025 + - 1080:1080 + wasm-manager: + image: maif/wasmo:1.0.16 + networks: + - manager-network + environment: + MANAGER_PORT: 5001 + AUTH_MODE: NO_AUTH + MANAGER_MAX_PARALLEL_JOBS: 2 + MANAGER_ALLOWED_DOMAINS: otoroshi.oto.tools,wasm-manager.oto.tools,localhost:5001 + MANAGER_EXPOSED DOMAINS: / + OTOROSHI_USER_HEADER: Otoroshi-User + WASMO_CLIENT_ID: admin-api-apikey-id + WASMO_CLIENT_SECRET: admin-api-apikey-secret + AWS_ACCESS_KEY_ID: AKIAIOSFODNN7EXAMPLE + AWS_SECRET_ACCESS_KEY: wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY + AWS_DEFAULT_REGION: us-east-1 + S3_FORCE_PATH_STYLE: 1 + S3_ENDPOINT: http://host.docker.internal:8000 + S3_BUCKET: wasm-manager + STORAGE: DOCKER_S3 + LOCAL_WASM_JOB_CLEANING: 60000 + ports: + - 5001:5001 + depends_on: + ninjaS3: + condition: service_healthy + links: + - ninjaS3 #s3Mock +# s3: +# image: scality/s3server +# networks: +# - manager-network +# environment: +# SCALITY_ACCESS_KEY_ID: access_key +# SCALITY_SECRET_ACCESS_KEY: secret +# ports: +# - 8000:8000 +# s3Mock: +# image: adobe/s3mock +# networks: +# - manager-network +# ports: +# - 8000:9090 +# environment: +# initialBuckets: wasm-manager +# healthcheck: +# test: ["CMD", "wget", "-O/dev/null", "-q", "http://localhost:9090/" ] +# interval: 2s +# timeout: 10s +# retries: 100 + ninjaS3: + image: scireum/s3-ninja:latest + networks: + - manager-network + ports: + - 8000:9000 + healthcheck: + test: curl --fail http://localhost:9000 || exit 1 + interval: 2s + timeout: 10s + retries: 100 +networks: + manager-network: \ No newline at end of file diff --git a/izanami-frontend/.eslintrc.js b/izanami-frontend/.eslintrc.js new file mode 100644 index 000000000..69c7e9c97 --- /dev/null +++ b/izanami-frontend/.eslintrc.js @@ -0,0 +1,28 @@ +module.exports = { + env: { + browser: true, + es2021: true, + }, + extends: [ + "eslint:recommended", + "plugin:react/recommended", + "plugin:@typescript-eslint/recommended", + ], + parser: "@typescript-eslint/parser", + parserOptions: { + ecmaFeatures: { + jsx: true, + }, + ecmaVersion: "latest", + sourceType: "module", + }, + plugins: ["react", "@typescript-eslint"], + rules: { + "@typescript-eslint/no-explicit-any": "off", + "prefer-const": "off", + "@typescript-eslint/no-non-null-assertion": "off", + "@typescript-eslint/no-namespace": "off", + "react/no-unescaped-entities": "off", + "react/display-name": "off", + }, +}; diff --git a/izanami-frontend/.github/workflows/playwright.yml b/izanami-frontend/.github/workflows/playwright.yml new file mode 100644 index 000000000..90b6b700d --- /dev/null +++ b/izanami-frontend/.github/workflows/playwright.yml @@ -0,0 +1,27 @@ +name: Playwright Tests +on: + push: + branches: [ main, master ] + pull_request: + branches: [ main, master ] +jobs: + test: + timeout-minutes: 60 + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-node@v3 + with: + node-version: 18 + - name: Install dependencies + run: npm ci + - name: Install Playwright Browsers + run: npx playwright install --with-deps + - name: Run Playwright tests + run: npx playwright test + - uses: actions/upload-artifact@v3 + if: always() + with: + name: playwright-report + path: playwright-report/ + retention-days: 30 diff --git a/izanami-frontend/.gitignore b/izanami-frontend/.gitignore new file mode 100644 index 000000000..f4cd97f53 --- /dev/null +++ b/izanami-frontend/.gitignore @@ -0,0 +1,32 @@ +/test-results/ +/playwright-report/ +/playwright + +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? +/test-results/ +/playwright-report/ +/playwright/.cache/ +stats.html diff --git a/izanami-frontend/android-chrome-192x192.png b/izanami-frontend/android-chrome-192x192.png new file mode 100644 index 000000000..5f2cbda17 Binary files /dev/null and b/izanami-frontend/android-chrome-192x192.png differ diff --git a/izanami-frontend/android-chrome-256x256.png b/izanami-frontend/android-chrome-256x256.png new file mode 100644 index 000000000..e3e4a174d Binary files /dev/null and b/izanami-frontend/android-chrome-256x256.png differ diff --git a/izanami-frontend/apple-touch-icon.png b/izanami-frontend/apple-touch-icon.png new file mode 100644 index 000000000..7deee194c Binary files /dev/null and b/izanami-frontend/apple-touch-icon.png differ diff --git a/izanami-frontend/browserconfig.xml b/izanami-frontend/browserconfig.xml new file mode 100644 index 000000000..b3930d0f0 --- /dev/null +++ b/izanami-frontend/browserconfig.xml @@ -0,0 +1,9 @@ + + + + + + #da532c + + + diff --git a/izanami-frontend/colors.txt b/izanami-frontend/colors.txt new file mode 100644 index 000000000..94325c1d3 --- /dev/null +++ b/izanami-frontend/colors.txt @@ -0,0 +1,20 @@ +primary: #DC5F9F + +sidebar text : #E6E6E6 + +menu hover : #8c8c8c + +couleur secondaire : #00B4CD; + +creation button : #5cb85c + +suppression button : #D5443F + +sidebar barckground : linear-gradient(180deg, #373735 1%, #494948) + +イザナミ + + +background: #373735; + +default color: #b5b3b3; \ No newline at end of file diff --git a/izanami-frontend/favicon-16x16.png b/izanami-frontend/favicon-16x16.png new file mode 100644 index 000000000..26cabd168 Binary files /dev/null and b/izanami-frontend/favicon-16x16.png differ diff --git a/izanami-frontend/favicon-32x32.png b/izanami-frontend/favicon-32x32.png new file mode 100644 index 000000000..a40204518 Binary files /dev/null and b/izanami-frontend/favicon-32x32.png differ diff --git a/izanami-frontend/favicon.ico b/izanami-frontend/favicon.ico new file mode 100644 index 000000000..69b14b200 Binary files /dev/null and b/izanami-frontend/favicon.ico differ diff --git a/izanami-frontend/index.html b/izanami-frontend/index.html new file mode 100644 index 000000000..e6c4ee7b4 --- /dev/null +++ b/izanami-frontend/index.html @@ -0,0 +1,42 @@ + + + + + + Izanami + + + + + + + + + + + + + +
+ + + + diff --git a/izanami-frontend/init.js b/izanami-frontend/init.js new file mode 100644 index 000000000..b28b62c96 --- /dev/null +++ b/izanami-frontend/init.js @@ -0,0 +1,44 @@ +//const pg = require("pg"); + +async function cleanDatabase() { + /*const { Client } = pg; + + const client = new Client({ + user: "postgres", + host: "localhost", + database: "postgres", + password: "postgres", + port: 5432, + }); + await client.connect(); + + try { + const res = await client.query("SELECT * FROM izanami.tenants"); + const tenants = res.rows.map((r) => r.name); + + tenants.forEach(async (tenant) => { + await client.query(`DROP SCHEMA "${tenant}" CASCADE`); + }); + + await client.query("TRUNCATE TABLE izanami.tenants CASCADE"); + + await client.query( + "DELETE FROM izanami.users WHERE username != 'RESERVED_ADMIN_USER'" + ); + await client.query("TRUNCATE TABLE izanami.users_tenants_rights CASCADE"); + } catch (err) { + if (err.code === "42P01") { + // Database is not initialized, nothing to do + } else { + throw err; + } + } + + await client.end();*/ + return Promise.resolve(); +} + +//init(); +module.exports = { + cleanDatabase, +}; diff --git a/izanami-frontend/izanami.png b/izanami-frontend/izanami.png new file mode 100644 index 000000000..28f0e4bef Binary files /dev/null and b/izanami-frontend/izanami.png differ diff --git a/izanami-frontend/izanami.svg b/izanami-frontend/izanami.svg new file mode 100644 index 000000000..8f5a5b2a4 --- /dev/null +++ b/izanami-frontend/izanami.svg @@ -0,0 +1,365 @@ + + + + +Fichier 1 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/izanami-frontend/mstile-150x150.png b/izanami-frontend/mstile-150x150.png new file mode 100644 index 000000000..d97984139 Binary files /dev/null and b/izanami-frontend/mstile-150x150.png differ diff --git a/izanami-frontend/package-lock.json b/izanami-frontend/package-lock.json new file mode 100644 index 000000000..c716d1a43 --- /dev/null +++ b/izanami-frontend/package-lock.json @@ -0,0 +1,11101 @@ +{ + "name": "izanami-frontend", + "version": "0.0.0", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "name": "izanami-frontend", + "version": "0.0.0", + "dependencies": { + "@emotion/react": "^11.11.1", + "@emotion/styled": "^11.11.0", + "@maif/react-forms": "1.6.3", + "@mui/material": "^5.13.6", + "@tanstack/react-table": "^8.1.3", + "@textea/json-viewer": "^3.1.1", + "@xstate/react": "^3.0.0", + "bootstrap": "^5.0.0", + "bootstrap-icons": "^1.8.2", + "classnames": "^2.3.0", + "date-fns": "^2.28.0", + "lodash": "^4.17.21", + "react": "18.1.0", + "react-dom": "18.1.0", + "react-hook-form": "^7.48.2", + "react-hot-toast": "^2.4.1", + "react-query": "^3.39.1", + "react-router-dom": "6.4.0", + "react-select": "^5.7.7", + "react-tooltip": "^5.14.0", + "uuid": "^8.3.2", + "xstate": "^4.32.1" + }, + "devDependencies": { + "@playwright/test": "^1.41.1", + "@types/jest": "^26.0.24", + "@types/node": "^16.3.0", + "@types/react-dom": "^18.0.5", + "@typescript-eslint/eslint-plugin": "^5.26.0", + "@typescript-eslint/parser": "^5.26.0", + "@vitejs/plugin-react": "^1.3.0", + "eslint": "^8.16.0", + "eslint-plugin-react": "^7.30.0", + "pg": "^8.11.3", + "prettier": "2.6.2", + "process": "^0.11.10", + "rollup-plugin-analyzer": "^4.0.0", + "rollup-plugin-visualizer": "^5.6.0", + "sass": "^1.55.0", + "typescript": "^4.6.3", + "vite": "^2.9.9" + } + }, + "node_modules/@ampproject/remapping": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.0.tgz", + "integrity": "sha512-qRmjj8nj9qmLTQXXmaR1cck3UXSRMPrbsLJAasZpF+t3riI71BXed5ebIOYwQntykeZuhjsdweEc9BxH5Jc26w==", + "dev": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.1.0", + "@jridgewell/trace-mapping": "^0.3.9" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.16.7.tgz", + "integrity": "sha512-iAXqUn8IIeBTNd72xsFlgaXHkMBMt6y4HJp1tIaK465CWLT/fG1aqB7ykr95gHHmlBdGbFeWWfyB4NJJ0nmeIg==", + "dependencies": { + "@babel/highlight": "^7.16.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.18.5", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.18.5.tgz", + "integrity": "sha512-BxhE40PVCBxVEJsSBhB6UWyAuqJRxGsAw8BdHMJ3AKGydcwuWW4kOO3HmqBQAdcq/OP+/DlTVxLvsCzRTnZuGg==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.18.5", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.18.5.tgz", + "integrity": "sha512-MGY8vg3DxMnctw0LdvSEojOsumc70g0t18gNyUdAZqB1Rpd1Bqo/svHGvt+UJ6JcGX+DIekGFDxxIWofBxLCnQ==", + "dev": true, + "dependencies": { + "@ampproject/remapping": "^2.1.0", + "@babel/code-frame": "^7.16.7", + "@babel/generator": "^7.18.2", + "@babel/helper-compilation-targets": "^7.18.2", + "@babel/helper-module-transforms": "^7.18.0", + "@babel/helpers": "^7.18.2", + "@babel/parser": "^7.18.5", + "@babel/template": "^7.16.7", + "@babel/traverse": "^7.18.5", + "@babel/types": "^7.18.4", + "convert-source-map": "^1.7.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.1", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/core/node_modules/semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/generator": { + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.18.2.tgz", + "integrity": "sha512-W1lG5vUwFvfMd8HVXqdfbuG7RuaSrTCCD8cl8fP8wOivdbtbIg2Db3IWUcgvfxKbbn6ZBGYRW/Zk1MIwK49mgw==", + "dev": true, + "dependencies": { + "@babel/types": "^7.18.2", + "@jridgewell/gen-mapping": "^0.3.0", + "jsesc": "^2.5.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/generator/node_modules/@jridgewell/gen-mapping": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.1.tgz", + "integrity": "sha512-GcHwniMlA2z+WFPWuY8lp3fsza0I8xPFMWL5+n8LYyP6PSvPrXf4+n8stDHZY2DM0zy9sVkRDy1jDI4XGzYVqg==", + "dev": true, + "dependencies": { + "@jridgewell/set-array": "^1.0.0", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.9" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/helper-annotate-as-pure": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.16.7.tgz", + "integrity": "sha512-s6t2w/IPQVTAET1HitoowRGXooX8mCgtuP5195wD/QJPV6wYjpujCGF7JuMODVX2ZAJOf1GT6DT9MHEZvLOFSw==", + "dev": true, + "dependencies": { + "@babel/types": "^7.16.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.18.2.tgz", + "integrity": "sha512-s1jnPotJS9uQnzFtiZVBUxe67CuBa679oWFHpxYYnTpRL/1ffhyX44R9uYiXoa/pLXcY9H2moJta0iaanlk/rQ==", + "dev": true, + "dependencies": { + "@babel/compat-data": "^7.17.10", + "@babel/helper-validator-option": "^7.16.7", + "browserslist": "^4.20.2", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-environment-visitor": { + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.18.2.tgz", + "integrity": "sha512-14GQKWkX9oJzPiQQ7/J36FTXcD4kSp8egKjO9nINlSKiHITRA9q/R74qu8S9xlc/b/yjsJItQUeeh3xnGN0voQ==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-function-name": { + "version": "7.17.9", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.17.9.tgz", + "integrity": "sha512-7cRisGlVtiVqZ0MW0/yFB4atgpGLWEHUVYnb448hZK4x+vih0YO5UoS11XIYtZYqHd0dIPMdUSv8q5K4LdMnIg==", + "dev": true, + "dependencies": { + "@babel/template": "^7.16.7", + "@babel/types": "^7.17.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-hoist-variables": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.16.7.tgz", + "integrity": "sha512-m04d/0Op34H5v7pbZw6pSKP7weA6lsMvfiIAMeIvkY/R4xQtBSMFEigu9QTZ2qB/9l22vsxtM8a+Q8CzD255fg==", + "dev": true, + "dependencies": { + "@babel/types": "^7.16.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.16.7.tgz", + "integrity": "sha512-LVtS6TqjJHFc+nYeITRo6VLXve70xmq7wPhWTqDJusJEgGmkAACWwMiTNrvfoQo6hEhFwAIixNkvB0jPXDL8Wg==", + "dependencies": { + "@babel/types": "^7.16.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.18.0.tgz", + "integrity": "sha512-kclUYSUBIjlvnzN2++K9f2qzYKFgjmnmjwL4zlmU5f8ZtzgWe8s0rUPSTGy2HmK4P8T52MQsS+HTQAgZd3dMEA==", + "dev": true, + "dependencies": { + "@babel/helper-environment-visitor": "^7.16.7", + "@babel/helper-module-imports": "^7.16.7", + "@babel/helper-simple-access": "^7.17.7", + "@babel/helper-split-export-declaration": "^7.16.7", + "@babel/helper-validator-identifier": "^7.16.7", + "@babel/template": "^7.16.7", + "@babel/traverse": "^7.18.0", + "@babel/types": "^7.18.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.17.12", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.17.12.tgz", + "integrity": "sha512-JDkf04mqtN3y4iAbO1hv9U2ARpPyPL1zqyWs/2WG1pgSq9llHFjStX5jdxb84himgJm+8Ng+x0oiWF/nw/XQKA==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-simple-access": { + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.18.2.tgz", + "integrity": "sha512-7LIrjYzndorDY88MycupkpQLKS1AFfsVRm2k/9PtKScSy5tZq0McZTj+DiMRynboZfIqOKvo03pmhTaUgiD6fQ==", + "dev": true, + "dependencies": { + "@babel/types": "^7.18.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-split-export-declaration": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.16.7.tgz", + "integrity": "sha512-xbWoy/PFoxSWazIToT9Sif+jJTlrMcndIsaOKvTA6u7QEo7ilkRZpjew18/W3c7nm8fXdUDXh02VXTbZ0pGDNw==", + "dev": true, + "dependencies": { + "@babel/types": "^7.16.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.16.7.tgz", + "integrity": "sha512-hsEnFemeiW4D08A5gUAZxLBTXpZ39P+a+DGDsHw1yxqyQ/jzFEnxf5uTEGp+3bzAbNOxU1paTgYS4ECU/IgfDw==", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.16.7.tgz", + "integrity": "sha512-TRtenOuRUVo9oIQGPC5G9DgK4743cdxvtOw0weQNpZXaS16SCBi5MNjZF8vba3ETURjZpTbVn7Vvcf2eAwFozQ==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.18.2.tgz", + "integrity": "sha512-j+d+u5xT5utcQSzrh9p+PaJX94h++KN+ng9b9WEJq7pkUPAd61FGqhjuUEdfknb3E/uDBb7ruwEeKkIxNJPIrg==", + "dev": true, + "dependencies": { + "@babel/template": "^7.16.7", + "@babel/traverse": "^7.18.2", + "@babel/types": "^7.18.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/highlight": { + "version": "7.17.12", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.17.12.tgz", + "integrity": "sha512-7yykMVF3hfZY2jsHZEEgLc+3x4o1O+fYyULu11GynEUQNwB6lua+IIQn1FiJxNucd5UlyJryrwsOh8PL9Sn8Qg==", + "dependencies": { + "@babel/helper-validator-identifier": "^7.16.7", + "chalk": "^2.0.0", + "js-tokens": "^4.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.18.5", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.18.5.tgz", + "integrity": "sha512-YZWVaglMiplo7v8f1oMQ5ZPQr0vn7HPeZXxXWsxXJRjGVrzUFn9OxFQl1sb5wzfootjA/yChhW84BV+383FSOw==", + "dev": true, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-syntax-jsx": { + "version": "7.17.12", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.17.12.tgz", + "integrity": "sha512-spyY3E3AURfxh/RHtjx5j6hs8am5NbUBGfcZ2vB3uShSpZdQyXSf5rR5Mk76vbtlAZOelyVQ71Fg0x9SG4fsog==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.17.12" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx": { + "version": "7.17.12", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.17.12.tgz", + "integrity": "sha512-Lcaw8bxd1DKht3thfD4A12dqo1X16he1Lm8rIv8sTwjAYNInRS1qHa9aJoqvzpscItXvftKDCfaEQzwoVyXpEQ==", + "dev": true, + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.16.7", + "@babel/helper-module-imports": "^7.16.7", + "@babel/helper-plugin-utils": "^7.17.12", + "@babel/plugin-syntax-jsx": "^7.17.12", + "@babel/types": "^7.17.12" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-development": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-development/-/plugin-transform-react-jsx-development-7.16.7.tgz", + "integrity": "sha512-RMvQWvpla+xy6MlBpPlrKZCMRs2AGiHOGHY3xRwl0pEeim348dDyxeH4xBsMPbIMhujeq7ihE702eM2Ew0Wo+A==", + "dev": true, + "dependencies": { + "@babel/plugin-transform-react-jsx": "^7.16.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.17.12", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.17.12.tgz", + "integrity": "sha512-7S9G2B44EnYOx74mue02t1uD8ckWZ/ee6Uz/qfdzc35uWHX5NgRy9i+iJSb2LFRgMd+QV9zNcStQaazzzZ3n3Q==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.17.12" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.16.7.tgz", + "integrity": "sha512-rONFiQz9vgbsnaMtQlZCjIRwhJvlrPET8TabIUK2hzlXw9B9s2Ieaxte1SCOOXMbWRHodbKixNf3BLcWVOQ8Bw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.16.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.22.5.tgz", + "integrity": "sha512-ecjvYlnAaZ/KVneE/OdKYBYfgXV3Ptu6zQWmgEF7vwKhQnvVS6bjMD2XYgj+SNvQ1GfK/pjgokfPkC/2CO8CuA==", + "dependencies": { + "regenerator-runtime": "^0.13.11" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/template": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.16.7.tgz", + "integrity": "sha512-I8j/x8kHUrbYRTUxXrrMbfCa7jxkE7tZre39x3kjr9hvI82cK1FfqLygotcWN5kdPGWcLdWMHpSBavse5tWw3w==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.16.7", + "@babel/parser": "^7.16.7", + "@babel/types": "^7.16.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.18.5", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.18.5.tgz", + "integrity": "sha512-aKXj1KT66sBj0vVzk6rEeAO6Z9aiiQ68wfDgge3nHhA/my6xMM/7HGQUNumKZaoa2qUPQ5whJG9aAifsxUKfLA==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.16.7", + "@babel/generator": "^7.18.2", + "@babel/helper-environment-visitor": "^7.18.2", + "@babel/helper-function-name": "^7.17.9", + "@babel/helper-hoist-variables": "^7.16.7", + "@babel/helper-split-export-declaration": "^7.16.7", + "@babel/parser": "^7.18.5", + "@babel/types": "^7.18.4", + "debug": "^4.1.0", + "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.18.4", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.18.4.tgz", + "integrity": "sha512-ThN1mBcMq5pG/Vm2IcBmPPfyPXbd8S02rS+OBIDENdufvqC7Z/jHPCv9IcP01277aKtDI8g/2XysBN4hA8niiw==", + "dependencies": { + "@babel/helper-validator-identifier": "^7.16.7", + "to-fast-properties": "^2.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@codemirror/autocomplete": { + "version": "0.19.15", + "resolved": "https://registry.npmjs.org/@codemirror/autocomplete/-/autocomplete-0.19.15.tgz", + "integrity": "sha512-GQWzvvuXxNUyaEk+5gawbAD8s51/v2Chb++nx0e2eGWrphWk42isBtzOMdc3DxrxrZtPZ55q2ldNp+6G8KJLIQ==", + "dependencies": { + "@codemirror/language": "^0.19.0", + "@codemirror/state": "^0.19.4", + "@codemirror/text": "^0.19.2", + "@codemirror/tooltip": "^0.19.12", + "@codemirror/view": "^0.19.0", + "@lezer/common": "^0.15.0" + } + }, + "node_modules/@codemirror/basic-setup": { + "version": "0.19.3", + "resolved": "https://registry.npmjs.org/@codemirror/basic-setup/-/basic-setup-0.19.3.tgz", + "integrity": "sha512-2hfO+QDk/HTpQzeYk1NyL1G9D5L7Sj78dtaQP8xBU42DKU9+OBPF5MdjLYnxP0jKzm6IfQfsLd89fnqW3rBVfQ==", + "dependencies": { + "@codemirror/autocomplete": "^0.19.0", + "@codemirror/closebrackets": "^0.19.0", + "@codemirror/commands": "^0.19.0", + "@codemirror/comment": "^0.19.0", + "@codemirror/fold": "^0.19.0", + "@codemirror/gutter": "^0.19.0", + "@codemirror/highlight": "^0.19.0", + "@codemirror/history": "^0.19.0", + "@codemirror/language": "^0.19.0", + "@codemirror/lint": "^0.19.0", + "@codemirror/matchbrackets": "^0.19.0", + "@codemirror/rectangular-selection": "^0.19.2", + "@codemirror/search": "^0.19.0", + "@codemirror/state": "^0.19.0", + "@codemirror/view": "^0.19.31" + } + }, + "node_modules/@codemirror/closebrackets": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/@codemirror/closebrackets/-/closebrackets-0.19.2.tgz", + "integrity": "sha512-ClMPzPcPP0eQiDcVjtVPl6OLxgdtZSYDazsvT0AKl70V1OJva0eHgl4/6kCW3RZ0pb2n34i9nJz4eXCmK+TYDA==", + "deprecated": "As of 0.20.0, this package has been merged into @codemirror/autocomplete", + "dependencies": { + "@codemirror/language": "^0.19.0", + "@codemirror/rangeset": "^0.19.0", + "@codemirror/state": "^0.19.2", + "@codemirror/text": "^0.19.0", + "@codemirror/view": "^0.19.44" + } + }, + "node_modules/@codemirror/commands": { + "version": "0.19.8", + "resolved": "https://registry.npmjs.org/@codemirror/commands/-/commands-0.19.8.tgz", + "integrity": "sha512-65LIMSGUGGpY3oH6mzV46YWRrgao6NmfJ+AuC7jNz3K5NPnH6GCV1H5I6SwOFyVbkiygGyd0EFwrWqywTBD1aw==", + "dependencies": { + "@codemirror/language": "^0.19.0", + "@codemirror/matchbrackets": "^0.19.0", + "@codemirror/state": "^0.19.2", + "@codemirror/text": "^0.19.6", + "@codemirror/view": "^0.19.22", + "@lezer/common": "^0.15.0" + } + }, + "node_modules/@codemirror/comment": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@codemirror/comment/-/comment-0.19.1.tgz", + "integrity": "sha512-uGKteBuVWAC6fW+Yt8u27DOnXMT/xV4Ekk2Z5mRsiADCZDqYvryrJd6PLL5+8t64BVyocwQwNfz1UswYS2CtFQ==", + "deprecated": "As of 0.20.0, this package has been merged into @codemirror/commands", + "dependencies": { + "@codemirror/state": "^0.19.9", + "@codemirror/text": "^0.19.0", + "@codemirror/view": "^0.19.0" + } + }, + "node_modules/@codemirror/fold": { + "version": "0.19.4", + "resolved": "https://registry.npmjs.org/@codemirror/fold/-/fold-0.19.4.tgz", + "integrity": "sha512-0SNSkRSOa6gymD6GauHa3sxiysjPhUC0SRVyTlvL52o0gz9GHdc8kNqNQskm3fBtGGOiSriGwF/kAsajRiGhVw==", + "deprecated": "As of 0.20.0, this package has been merged into @codemirror/language", + "dependencies": { + "@codemirror/gutter": "^0.19.0", + "@codemirror/language": "^0.19.0", + "@codemirror/rangeset": "^0.19.0", + "@codemirror/state": "^0.19.0", + "@codemirror/view": "^0.19.22" + } + }, + "node_modules/@codemirror/gutter": { + "version": "0.19.9", + "resolved": "https://registry.npmjs.org/@codemirror/gutter/-/gutter-0.19.9.tgz", + "integrity": "sha512-PFrtmilahin1g6uL27aG5tM/rqR9DZzZYZsIrCXA5Uc2OFTFqx4owuhoU9hqfYxHp5ovfvBwQ+txFzqS4vog6Q==", + "deprecated": "As of 0.20.0, this package has been merged into @codemirror/view", + "dependencies": { + "@codemirror/rangeset": "^0.19.0", + "@codemirror/state": "^0.19.0", + "@codemirror/view": "^0.19.23" + } + }, + "node_modules/@codemirror/highlight": { + "version": "0.19.8", + "resolved": "https://registry.npmjs.org/@codemirror/highlight/-/highlight-0.19.8.tgz", + "integrity": "sha512-v/lzuHjrYR8MN2mEJcUD6fHSTXXli9C1XGYpr+ElV6fLBIUhMTNKR3qThp611xuWfXfwDxeL7ppcbkM/MzPV3A==", + "deprecated": "As of 0.20.0, this package has been split between @lezer/highlight and @codemirror/language", + "dependencies": { + "@codemirror/language": "^0.19.0", + "@codemirror/rangeset": "^0.19.0", + "@codemirror/state": "^0.19.3", + "@codemirror/view": "^0.19.39", + "@lezer/common": "^0.15.0", + "style-mod": "^4.0.0" + } + }, + "node_modules/@codemirror/history": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/@codemirror/history/-/history-0.19.2.tgz", + "integrity": "sha512-unhP4t3N2smzmHoo/Yio6ueWi+il8gm9VKrvi6wlcdGH5fOfVDNkmjHQ495SiR+EdOG35+3iNebSPYww0vN7ow==", + "deprecated": "As of 0.20.0, this package has been merged into @codemirror/commands", + "dependencies": { + "@codemirror/state": "^0.19.2", + "@codemirror/view": "^0.19.0" + } + }, + "node_modules/@codemirror/lang-cpp": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@codemirror/lang-cpp/-/lang-cpp-0.19.1.tgz", + "integrity": "sha512-BGvZkfcqcalAwxocuE9DhH6gqflm5IjL/8mGTzc8bHzeP1N4innK8qo2G69ohEML4LDZv4WyXc3y4C9/zsGCGQ==", + "dependencies": { + "@codemirror/highlight": "^0.19.0", + "@codemirror/language": "^0.19.0", + "@lezer/cpp": "^0.15.0" + } + }, + "node_modules/@codemirror/lang-css": { + "version": "0.19.3", + "resolved": "https://registry.npmjs.org/@codemirror/lang-css/-/lang-css-0.19.3.tgz", + "integrity": "sha512-tyCUJR42/UlfOPLb94/p7dN+IPsYSIzHbAHP2KQHANj0I+Orqp+IyIOS++M8TuCX4zkWh9dvi8s92yy/Tn8Ifg==", + "dependencies": { + "@codemirror/autocomplete": "^0.19.0", + "@codemirror/highlight": "^0.19.6", + "@codemirror/language": "^0.19.0", + "@codemirror/state": "^0.19.0", + "@lezer/css": "^0.15.2" + } + }, + "node_modules/@codemirror/lang-html": { + "version": "0.19.4", + "resolved": "https://registry.npmjs.org/@codemirror/lang-html/-/lang-html-0.19.4.tgz", + "integrity": "sha512-GpiEikNuCBeFnS+/TJSeanwqaOfNm8Kkp9WpVNEPZCLyW1mAMCuFJu/3xlWYeWc778Hc3vJqGn3bn+cLNubgCA==", + "dependencies": { + "@codemirror/autocomplete": "^0.19.0", + "@codemirror/highlight": "^0.19.6", + "@codemirror/lang-css": "^0.19.0", + "@codemirror/lang-javascript": "^0.19.0", + "@codemirror/language": "^0.19.0", + "@codemirror/state": "^0.19.0", + "@lezer/common": "^0.15.0", + "@lezer/html": "^0.15.0" + } + }, + "node_modules/@codemirror/lang-java": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@codemirror/lang-java/-/lang-java-0.19.1.tgz", + "integrity": "sha512-yA3kcW2GgY0mC2a9dE+uRxGxPWeykfE/GqEPk4TSmhuU4ndmyDgM5QQP7pgnYSZmv2vKoyf4x7NMg8AF7lKXHQ==", + "dependencies": { + "@codemirror/highlight": "^0.19.0", + "@codemirror/language": "^0.19.0", + "@lezer/java": "^0.15.0" + } + }, + "node_modules/@codemirror/lang-javascript": { + "version": "0.19.7", + "resolved": "https://registry.npmjs.org/@codemirror/lang-javascript/-/lang-javascript-0.19.7.tgz", + "integrity": "sha512-DL9f3JLqOEHH9cIwEqqjnP5bkjdVXeECksLtV+/MbPm+l4H+AG+PkwZaJQ2oR1GfPZKh8MVSIE94aGWNkJP8WQ==", + "dependencies": { + "@codemirror/autocomplete": "^0.19.0", + "@codemirror/highlight": "^0.19.7", + "@codemirror/language": "^0.19.0", + "@codemirror/lint": "^0.19.0", + "@codemirror/state": "^0.19.0", + "@codemirror/view": "^0.19.0", + "@lezer/javascript": "^0.15.1" + } + }, + "node_modules/@codemirror/lang-json": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/@codemirror/lang-json/-/lang-json-0.19.2.tgz", + "integrity": "sha512-fgUWR58Is59P5D/tiazX6oTczioOCDYqjFT5PEBAmLBFMSsRqcnJE0xNO1snrhg7pWEFDq5wR/oN0eZhkeR6Gg==", + "dependencies": { + "@codemirror/highlight": "^0.19.0", + "@codemirror/language": "^0.19.0", + "@lezer/json": "^0.15.0" + } + }, + "node_modules/@codemirror/lang-markdown": { + "version": "0.19.6", + "resolved": "https://registry.npmjs.org/@codemirror/lang-markdown/-/lang-markdown-0.19.6.tgz", + "integrity": "sha512-ojoHeLgv1Rfu0GNGsU0bCtXAIp5dy4VKjndHScITQdlCkS/+SAIfuoeowEx+nMAQwTxI+/9fQZ3xdZVznGFYug==", + "dependencies": { + "@codemirror/highlight": "^0.19.0", + "@codemirror/lang-html": "^0.19.0", + "@codemirror/language": "^0.19.0", + "@codemirror/state": "^0.19.3", + "@codemirror/view": "^0.19.0", + "@lezer/common": "^0.15.0", + "@lezer/markdown": "^0.15.0" + } + }, + "node_modules/@codemirror/lang-php": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@codemirror/lang-php/-/lang-php-0.19.1.tgz", + "integrity": "sha512-Q6djLACHu1J6XbnxWlEPCiyqqDrlZLi9QtjY6b9vqdkq/GOsNaXVv44nDY8DD6Bxi5yYRTJ3yh8XzsKuJgztjQ==", + "dependencies": { + "@codemirror/highlight": "^0.19.0", + "@codemirror/lang-html": "^0.19.0", + "@codemirror/language": "^0.19.0", + "@codemirror/state": "^0.19.0", + "@lezer/common": "^0.15.0", + "@lezer/php": "^0.15.0" + } + }, + "node_modules/@codemirror/lang-python": { + "version": "0.19.5", + "resolved": "https://registry.npmjs.org/@codemirror/lang-python/-/lang-python-0.19.5.tgz", + "integrity": "sha512-MQf7t0k6+i9KCzlFCI8EY+jjwyXLy5AwjmXsMyMCMbOw/97j70jFZYrs7Mm7RJakNE2rypWhnLGlyBTSYMqR5g==", + "dependencies": { + "@codemirror/highlight": "^0.19.7", + "@codemirror/language": "^0.19.0", + "@lezer/python": "^0.15.0" + } + }, + "node_modules/@codemirror/lang-rust": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/@codemirror/lang-rust/-/lang-rust-0.19.2.tgz", + "integrity": "sha512-SEXsO7Qf2gktRvVhHMc0Mq4HzPBpFcQlrlcinafy6VFXavWs+QAIB8UAuLG/igOc3PrIHbZFlyEhVUIGstox8w==", + "dependencies": { + "@codemirror/highlight": "^0.19.7", + "@codemirror/language": "^0.19.0", + "@lezer/rust": "^0.15.0" + } + }, + "node_modules/@codemirror/lang-sql": { + "version": "0.19.4", + "resolved": "https://registry.npmjs.org/@codemirror/lang-sql/-/lang-sql-0.19.4.tgz", + "integrity": "sha512-4FqLC8aNe1iCDyAWbJmSqa8K7rgz2xTwW36V35z4oiyLoyOLsCayKIwoQqp5DNIq2ckGCsyzotgxXKpgtg/pgg==", + "dependencies": { + "@codemirror/autocomplete": "^0.19.0", + "@codemirror/highlight": "^0.19.0", + "@codemirror/language": "^0.19.0", + "@codemirror/state": "^0.19.0", + "@lezer/lr": "^0.15.0" + } + }, + "node_modules/@codemirror/lang-wast": { + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/@codemirror/lang-wast/-/lang-wast-0.19.0.tgz", + "integrity": "sha512-mr/Bp4k8+fJ0P8/Q6L45pnX7/bDBk4VP8ahYrTdvHo+UaOqBBhBFtBqBikvX8ZDQiUTfuZ4tnJE2QtOvmFsuzg==", + "dependencies": { + "@codemirror/highlight": "^0.19.0", + "@codemirror/language": "^0.19.0", + "@lezer/lr": "^0.15.0" + } + }, + "node_modules/@codemirror/lang-xml": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/@codemirror/lang-xml/-/lang-xml-0.19.2.tgz", + "integrity": "sha512-9VIjxvqcH1sk8bmYbxQon0lXhVZgdHdfjGes+e4Akgvb43aMBDNvIQVALwrCb+XMEHTxLUMQtrsBN0G64yCUXw==", + "dependencies": { + "@codemirror/autocomplete": "^0.19.0", + "@codemirror/highlight": "^0.19.6", + "@codemirror/language": "^0.19.0", + "@codemirror/state": "^0.19.0", + "@lezer/common": "^0.15.0", + "@lezer/xml": "^0.15.0" + } + }, + "node_modules/@codemirror/language": { + "version": "0.19.10", + "resolved": "https://registry.npmjs.org/@codemirror/language/-/language-0.19.10.tgz", + "integrity": "sha512-yA0DZ3RYn2CqAAGW62VrU8c4YxscMQn45y/I9sjBlqB1e2OTQLg4CCkMBuMSLXk4xaqjlsgazeOQWaJQOKfV8Q==", + "dependencies": { + "@codemirror/state": "^0.19.0", + "@codemirror/text": "^0.19.0", + "@codemirror/view": "^0.19.0", + "@lezer/common": "^0.15.5", + "@lezer/lr": "^0.15.0" + } + }, + "node_modules/@codemirror/language-data": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/@codemirror/language-data/-/language-data-0.19.2.tgz", + "integrity": "sha512-O38TaBfzqs5vK8Z+ZlAmaGqciQxgtAXacOTSq22ZLrsKmYMbeFZNHCqDL6VMG2wOt1jtRnfJD56chONwaPRUVQ==", + "dependencies": { + "@codemirror/lang-cpp": "^0.19.0", + "@codemirror/lang-css": "^0.19.0", + "@codemirror/lang-html": "^0.19.0", + "@codemirror/lang-java": "^0.19.0", + "@codemirror/lang-javascript": "^0.19.0", + "@codemirror/lang-json": "^0.19.0", + "@codemirror/lang-markdown": "^0.19.0", + "@codemirror/lang-php": "^0.19.0", + "@codemirror/lang-python": "^0.19.0", + "@codemirror/lang-rust": "^0.19.0", + "@codemirror/lang-sql": "^0.19.0", + "@codemirror/lang-wast": "^0.19.0", + "@codemirror/lang-xml": "^0.19.0", + "@codemirror/language": "^0.19.0", + "@codemirror/legacy-modes": "^0.19.0", + "@codemirror/stream-parser": "^0.19.0" + } + }, + "node_modules/@codemirror/legacy-modes": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@codemirror/legacy-modes/-/legacy-modes-0.19.1.tgz", + "integrity": "sha512-vYPLsD/ON+3SXhlGj9Qb3fpFNNU3Ya/AtDiv/g3OyqVzhh5vs5rAnOvk8xopGWRwppdhlNPD9VyXjiOmZUQtmQ==", + "dependencies": { + "@codemirror/stream-parser": "^0.19.0" + } + }, + "node_modules/@codemirror/lint": { + "version": "0.19.6", + "resolved": "https://registry.npmjs.org/@codemirror/lint/-/lint-0.19.6.tgz", + "integrity": "sha512-Pbw1Y5kHVs2J+itQ0uez3dI4qY9ApYVap7eNfV81x1/3/BXgBkKfadaw0gqJ4h4FDG7OnJwb0VbPsjJQllHjaA==", + "dependencies": { + "@codemirror/gutter": "^0.19.4", + "@codemirror/panel": "^0.19.0", + "@codemirror/rangeset": "^0.19.1", + "@codemirror/state": "^0.19.4", + "@codemirror/tooltip": "^0.19.16", + "@codemirror/view": "^0.19.22", + "crelt": "^1.0.5" + } + }, + "node_modules/@codemirror/matchbrackets": { + "version": "0.19.4", + "resolved": "https://registry.npmjs.org/@codemirror/matchbrackets/-/matchbrackets-0.19.4.tgz", + "integrity": "sha512-VFkaOKPNudAA5sGP1zikRHCEKU0hjYmkKpr04pybUpQvfTvNJXlReCyP0rvH/1iEwAGPL990ZTT+QrLdu4MeEA==", + "deprecated": "As of 0.20.0, this package has been merged into @codemirror/language", + "dependencies": { + "@codemirror/language": "^0.19.0", + "@codemirror/state": "^0.19.0", + "@codemirror/view": "^0.19.0", + "@lezer/common": "^0.15.0" + } + }, + "node_modules/@codemirror/panel": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@codemirror/panel/-/panel-0.19.1.tgz", + "integrity": "sha512-sYeOCMA3KRYxZYJYn5PNlt9yNsjy3zTNTrbYSfVgjgL9QomIVgOJWPO5hZ2sTN8lufO6lw0vTBsIPL9MSidmBg==", + "deprecated": "As of 0.20.0, this package has been merged into @codemirror/view", + "dependencies": { + "@codemirror/state": "^0.19.0", + "@codemirror/view": "^0.19.0" + } + }, + "node_modules/@codemirror/rangeset": { + "version": "0.19.9", + "resolved": "https://registry.npmjs.org/@codemirror/rangeset/-/rangeset-0.19.9.tgz", + "integrity": "sha512-V8YUuOvK+ew87Xem+71nKcqu1SXd5QROMRLMS/ljT5/3MCxtgrRie1Cvild0G/Z2f1fpWxzX78V0U4jjXBorBQ==", + "deprecated": "As of 0.20.0, this package has been merged into @codemirror/state", + "dependencies": { + "@codemirror/state": "^0.19.0" + } + }, + "node_modules/@codemirror/rectangular-selection": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/@codemirror/rectangular-selection/-/rectangular-selection-0.19.2.tgz", + "integrity": "sha512-AXK/p5eGwFJ9GJcLfntqN4dgY+XiIF7eHfXNQJX5HhQLSped2wJE6WuC1rMEaOlcpOqlb9mrNi/ZdUjSIj9mbA==", + "deprecated": "As of 0.20.0, this package has been merged into @codemirror/view", + "dependencies": { + "@codemirror/state": "^0.19.0", + "@codemirror/text": "^0.19.4", + "@codemirror/view": "^0.19.48" + } + }, + "node_modules/@codemirror/search": { + "version": "0.19.10", + "resolved": "https://registry.npmjs.org/@codemirror/search/-/search-0.19.10.tgz", + "integrity": "sha512-qjubm69HJixPBWzI6HeEghTWOOD8NXiHOTRNvdizqs8xWRuFChq9zkjD3XiAJ7GXSTzCuQJnAP9DBBGCLq4ZIA==", + "dependencies": { + "@codemirror/panel": "^0.19.0", + "@codemirror/rangeset": "^0.19.0", + "@codemirror/state": "^0.19.3", + "@codemirror/text": "^0.19.0", + "@codemirror/view": "^0.19.34", + "crelt": "^1.0.5" + } + }, + "node_modules/@codemirror/state": { + "version": "0.19.9", + "resolved": "https://registry.npmjs.org/@codemirror/state/-/state-0.19.9.tgz", + "integrity": "sha512-psOzDolKTZkx4CgUqhBQ8T8gBc0xN5z4gzed109aF6x7D7umpDRoimacI/O6d9UGuyl4eYuDCZmDFr2Rq7aGOw==", + "dependencies": { + "@codemirror/text": "^0.19.0" + } + }, + "node_modules/@codemirror/stream-parser": { + "version": "0.19.9", + "resolved": "https://registry.npmjs.org/@codemirror/stream-parser/-/stream-parser-0.19.9.tgz", + "integrity": "sha512-WTmkEFSRCetpk8xIOvV2yyXdZs3DgYckM0IP7eFi4ewlxWnJO/H4BeJZLs4wQaydWsAqTQoDyIwNH1BCzK5LUQ==", + "deprecated": "As of 0.20.0, this package has been merged into @codemirror/language", + "dependencies": { + "@codemirror/highlight": "^0.19.0", + "@codemirror/language": "^0.19.0", + "@codemirror/state": "^0.19.0", + "@codemirror/text": "^0.19.0", + "@lezer/common": "^0.15.0", + "@lezer/lr": "^0.15.0" + } + }, + "node_modules/@codemirror/text": { + "version": "0.19.6", + "resolved": "https://registry.npmjs.org/@codemirror/text/-/text-0.19.6.tgz", + "integrity": "sha512-T9jnREMIygx+TPC1bOuepz18maGq/92q2a+n4qTqObKwvNMg+8cMTslb8yxeEDEq7S3kpgGWxgO1UWbQRij0dA==", + "deprecated": "As of 0.20.0, this package has been merged into @codemirror/state" + }, + "node_modules/@codemirror/theme-one-dark": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@codemirror/theme-one-dark/-/theme-one-dark-0.19.1.tgz", + "integrity": "sha512-8gc4c2k2o/EhyHoWkghCxp5vyDT96JaFGtRy35PHwIom0LZdx7aU4AbDUnITvwiFB+0+i54VO+WQjBqgTyJvqg==", + "dependencies": { + "@codemirror/highlight": "^0.19.0", + "@codemirror/state": "^0.19.0", + "@codemirror/view": "^0.19.0" + } + }, + "node_modules/@codemirror/tooltip": { + "version": "0.19.16", + "resolved": "https://registry.npmjs.org/@codemirror/tooltip/-/tooltip-0.19.16.tgz", + "integrity": "sha512-zxKDHryUV5/RS45AQL+wOeN+i7/l81wK56OMnUPoTSzCWNITfxHn7BToDsjtrRKbzHqUxKYmBnn/4hPjpZ4WJQ==", + "deprecated": "As of 0.20.0, this package has been merged into @codemirror/view", + "dependencies": { + "@codemirror/state": "^0.19.0", + "@codemirror/view": "^0.19.0" + } + }, + "node_modules/@codemirror/view": { + "version": "0.19.48", + "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-0.19.48.tgz", + "integrity": "sha512-0eg7D2Nz4S8/caetCTz61rK0tkHI17V/d15Jy0kLOT8dTLGGNJUponDnW28h2B6bERmPlVHKh8MJIr5OCp1nGw==", + "dependencies": { + "@codemirror/rangeset": "^0.19.5", + "@codemirror/state": "^0.19.3", + "@codemirror/text": "^0.19.0", + "style-mod": "^4.0.0", + "w3c-keyname": "^2.2.4" + } + }, + "node_modules/@date-io/core": { + "version": "2.14.0", + "resolved": "https://registry.npmjs.org/@date-io/core/-/core-2.14.0.tgz", + "integrity": "sha512-qFN64hiFjmlDHJhu+9xMkdfDG2jLsggNxKXglnekUpXSq8faiqZgtHm2lsHCUuaPDTV6wuXHcCl8J1GQ5wLmPw==" + }, + "node_modules/@date-io/date-fns": { + "version": "2.14.0", + "resolved": "https://registry.npmjs.org/@date-io/date-fns/-/date-fns-2.14.0.tgz", + "integrity": "sha512-4fJctdVyOd5cKIKGaWUM+s3MUXMuzkZaHuTY15PH70kU1YTMrCoauA7hgQVx9qj0ZEbGrH9VSPYJYnYro7nKiA==", + "dependencies": { + "@date-io/core": "^2.14.0" + }, + "peerDependencies": { + "date-fns": "^2.0.0" + }, + "peerDependenciesMeta": { + "date-fns": { + "optional": true + } + } + }, + "node_modules/@date-io/dayjs": { + "version": "2.14.0", + "resolved": "https://registry.npmjs.org/@date-io/dayjs/-/dayjs-2.14.0.tgz", + "integrity": "sha512-4fRvNWaOh7AjvOyJ4h6FYMS7VHLQnIEeAV5ahv6sKYWx+1g1UwYup8h7+gPuoF+sW2hTScxi7PVaba2Jk/U8Og==", + "dependencies": { + "@date-io/core": "^2.14.0" + }, + "peerDependencies": { + "dayjs": "^1.8.17" + }, + "peerDependenciesMeta": { + "dayjs": { + "optional": true + } + } + }, + "node_modules/@date-io/luxon": { + "version": "2.14.0", + "resolved": "https://registry.npmjs.org/@date-io/luxon/-/luxon-2.14.0.tgz", + "integrity": "sha512-KmpBKkQFJ/YwZgVd0T3h+br/O0uL9ZdE7mn903VPAG2ZZncEmaUfUdYKFT7v7GyIKJ4KzCp379CRthEbxevEVg==", + "dependencies": { + "@date-io/core": "^2.14.0" + }, + "peerDependencies": { + "luxon": "^1.21.3 || ^2.x" + }, + "peerDependenciesMeta": { + "luxon": { + "optional": true + } + } + }, + "node_modules/@date-io/moment": { + "version": "2.14.0", + "resolved": "https://registry.npmjs.org/@date-io/moment/-/moment-2.14.0.tgz", + "integrity": "sha512-VsoLXs94GsZ49ecWuvFbsa081zEv2xxG7d+izJsqGa2L8RPZLlwk27ANh87+SNnOUpp+qy2AoCAf0mx4XXhioA==", + "dependencies": { + "@date-io/core": "^2.14.0" + }, + "peerDependencies": { + "moment": "^2.24.0" + }, + "peerDependenciesMeta": { + "moment": { + "optional": true + } + } + }, + "node_modules/@emotion/babel-plugin": { + "version": "11.11.0", + "resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.11.0.tgz", + "integrity": "sha512-m4HEDZleaaCH+XgDDsPF15Ht6wTLsgDTeR3WYj9Q/k76JtWhrJjcP4+/XlG8LGT/Rol9qUfOIztXeA84ATpqPQ==", + "dependencies": { + "@babel/helper-module-imports": "^7.16.7", + "@babel/runtime": "^7.18.3", + "@emotion/hash": "^0.9.1", + "@emotion/memoize": "^0.8.1", + "@emotion/serialize": "^1.1.2", + "babel-plugin-macros": "^3.1.0", + "convert-source-map": "^1.5.0", + "escape-string-regexp": "^4.0.0", + "find-root": "^1.1.0", + "source-map": "^0.5.7", + "stylis": "4.2.0" + } + }, + "node_modules/@emotion/babel-plugin/node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@emotion/babel-plugin/node_modules/source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@emotion/cache": { + "version": "11.11.0", + "resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-11.11.0.tgz", + "integrity": "sha512-P34z9ssTCBi3e9EI1ZsWpNHcfY1r09ZO0rZbRO2ob3ZQMnFI35jB536qoXbkdesr5EUhYi22anuEJuyxifaqAQ==", + "dependencies": { + "@emotion/memoize": "^0.8.1", + "@emotion/sheet": "^1.2.2", + "@emotion/utils": "^1.2.1", + "@emotion/weak-memoize": "^0.3.1", + "stylis": "4.2.0" + } + }, + "node_modules/@emotion/hash": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.1.tgz", + "integrity": "sha512-gJB6HLm5rYwSLI6PQa+X1t5CFGrv1J1TWG+sOyMCeKz2ojaj6Fnl/rZEspogG+cvqbt4AE/2eIyD2QfLKTBNlQ==" + }, + "node_modules/@emotion/is-prop-valid": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.2.1.tgz", + "integrity": "sha512-61Mf7Ufx4aDxx1xlDeOm8aFFigGHE4z+0sKCa+IHCeZKiyP9RLD0Mmx7m8b9/Cf37f7NAvQOOJAbQQGVr5uERw==", + "dependencies": { + "@emotion/memoize": "^0.8.1" + } + }, + "node_modules/@emotion/memoize": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.8.1.tgz", + "integrity": "sha512-W2P2c/VRW1/1tLox0mVUalvnWXxavmv/Oum2aPsRcoDJuob75FC3Y8FbpfLwUegRcxINtGUMPq0tFCvYNTBXNA==" + }, + "node_modules/@emotion/react": { + "version": "11.11.1", + "resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.11.1.tgz", + "integrity": "sha512-5mlW1DquU5HaxjLkfkGN1GA/fvVGdyHURRiX/0FHl2cfIfRxSOfmxEH5YS43edp0OldZrZ+dkBKbngxcNCdZvA==", + "dependencies": { + "@babel/runtime": "^7.18.3", + "@emotion/babel-plugin": "^11.11.0", + "@emotion/cache": "^11.11.0", + "@emotion/serialize": "^1.1.2", + "@emotion/use-insertion-effect-with-fallbacks": "^1.0.1", + "@emotion/utils": "^1.2.1", + "@emotion/weak-memoize": "^0.3.1", + "hoist-non-react-statics": "^3.3.1" + }, + "peerDependencies": { + "react": ">=16.8.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@emotion/serialize": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.1.2.tgz", + "integrity": "sha512-zR6a/fkFP4EAcCMQtLOhIgpprZOwNmCldtpaISpvz348+DP4Mz8ZoKaGGCQpbzepNIUWbq4w6hNZkwDyKoS+HA==", + "dependencies": { + "@emotion/hash": "^0.9.1", + "@emotion/memoize": "^0.8.1", + "@emotion/unitless": "^0.8.1", + "@emotion/utils": "^1.2.1", + "csstype": "^3.0.2" + } + }, + "node_modules/@emotion/sheet": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@emotion/sheet/-/sheet-1.2.2.tgz", + "integrity": "sha512-0QBtGvaqtWi+nx6doRwDdBIzhNdZrXUppvTM4dtZZWEGTXL/XE/yJxLMGlDT1Gt+UHH5IX1n+jkXyytE/av7OA==" + }, + "node_modules/@emotion/styled": { + "version": "11.11.0", + "resolved": "https://registry.npmjs.org/@emotion/styled/-/styled-11.11.0.tgz", + "integrity": "sha512-hM5Nnvu9P3midq5aaXj4I+lnSfNi7Pmd4EWk1fOZ3pxookaQTNew6bp4JaCBYM4HVFZF9g7UjJmsUmC2JlxOng==", + "dependencies": { + "@babel/runtime": "^7.18.3", + "@emotion/babel-plugin": "^11.11.0", + "@emotion/is-prop-valid": "^1.2.1", + "@emotion/serialize": "^1.1.2", + "@emotion/use-insertion-effect-with-fallbacks": "^1.0.1", + "@emotion/utils": "^1.2.1" + }, + "peerDependencies": { + "@emotion/react": "^11.0.0-rc.0", + "react": ">=16.8.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@emotion/unitless": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.8.1.tgz", + "integrity": "sha512-KOEGMu6dmJZtpadb476IsZBclKvILjopjUii3V+7MnXIQCYh8W3NgNcgwo21n9LXZX6EDIKvqfjYxXebDwxKmQ==" + }, + "node_modules/@emotion/use-insertion-effect-with-fallbacks": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@emotion/use-insertion-effect-with-fallbacks/-/use-insertion-effect-with-fallbacks-1.0.1.tgz", + "integrity": "sha512-jT/qyKZ9rzLErtrjGgdkMBn2OP8wl0G3sQlBb3YPryvKHsjvINUhVaPFfP+fpBcOkmrVOVEEHQFJ7nbj2TH2gw==", + "peerDependencies": { + "react": ">=16.8.0" + } + }, + "node_modules/@emotion/utils": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-1.2.1.tgz", + "integrity": "sha512-Y2tGf3I+XVnajdItskUCn6LX+VUDmP6lTL4fcqsXAv43dnlbZiuW4MWQW38rW/BVWSE7Q/7+XQocmpnRYILUmg==" + }, + "node_modules/@emotion/weak-memoize": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.3.1.tgz", + "integrity": "sha512-EsBwpc7hBUJWAsNPBmJy4hxWx12v6bshQsldrVmjxJoc3isbxhOrF2IcCpaXxfvq03NwkI7sbsOLXbYuqF/8Ww==" + }, + "node_modules/@eslint/eslintrc": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.3.0.tgz", + "integrity": "sha512-UWW0TMTmk2d7hLcWD1/e2g5HDM/HQ3csaLSqXCfqwh4uNDuNqlaKWXmEsL4Cs41Z0KnILNvwbHAah3C2yt06kw==", + "dev": true, + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^9.3.2", + "globals": "^13.15.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "13.15.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.15.0.tgz", + "integrity": "sha512-bpzcOlgDhMG070Av0Vy5Owklpv1I6+j96GhUI7Rh7IzDCKLzboflLrrfqMu8NquDbiR4EOQk7XzJwqVJxicxog==", + "dev": true, + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@floating-ui/core": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.3.1.tgz", + "integrity": "sha512-Bu+AMaXNjrpjh41znzHqaz3r2Nr8hHuHZT6V2LBKMhyMl0FgKA62PNYbqnfgmzOhoWZj70Zecisbo4H1rotP5g==" + }, + "node_modules/@floating-ui/dom": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.4.1.tgz", + "integrity": "sha512-loCXUOLzIC3jp50RFOKXZ/kQjjz26ryr/23M+FWG9jrmAv8lRf3DUfC2AiVZ3+K316GOhB08CR+Povwz8e9mDw==", + "dependencies": { + "@floating-ui/core": "^1.3.1" + } + }, + "node_modules/@fortawesome/fontawesome-free": { + "version": "5.15.4", + "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-free/-/fontawesome-free-5.15.4.tgz", + "integrity": "sha512-eYm8vijH/hpzr/6/1CJ/V/Eb1xQFW2nnUKArb3z+yUWv7HTwj6M7SP957oMjfZjAHU6qpoNc2wQvIxBLWYa/Jg==", + "hasInstallScript": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/@hookform/resolvers": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-2.4.0.tgz", + "integrity": "sha512-KiHc7Uwd2IJMvPTMQ9vQxfss2ulq2gRYL/HYZ90qiTs+07UgGWCikiIvE2pKjjGVltEYjq5eR8x0ITmoyEjGxQ==", + "peerDependencies": { + "react-hook-form": "^7.0.0" + } + }, + "node_modules/@humanwhocodes/config-array": { + "version": "0.9.5", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.9.5.tgz", + "integrity": "sha512-ObyMyWxZiCu/yTisA7uzx81s40xR2fD5Cg/2Kq7G02ajkNubJf6BopgDTmDyc3U7sXpNKM8cYOw7s7Tyr+DnCw==", + "dev": true, + "dependencies": { + "@humanwhocodes/object-schema": "^1.2.1", + "debug": "^4.1.1", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=10.10.0" + } + }, + "node_modules/@humanwhocodes/object-schema": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz", + "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==", + "dev": true + }, + "node_modules/@jest/types": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-26.6.2.tgz", + "integrity": "sha512-fC6QCp7Sc5sX6g8Tvbmj4XUTbyrik0akgRy03yjXbQaBWWNWGE7SGtJk98m0N8nzegD/7SggrUlivxo5ax4KWQ==", + "dev": true, + "dependencies": { + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^15.0.0", + "chalk": "^4.0.0" + }, + "engines": { + "node": ">= 10.14.2" + } + }, + "node_modules/@jest/types/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@jest/types/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/@jest/types/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/@jest/types/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/@jest/types/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/types/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/@jridgewell/gen-mapping": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.1.1.tgz", + "integrity": "sha512-sQXCasFk+U8lWYEe66WxRDOE9PjVz4vSM51fTu3Hw+ClTpUSQb718772vH3pyS5pShp6lvQM7SxgIDXXXmOX7w==", + "dev": true, + "dependencies": { + "@jridgewell/set-array": "^1.0.0", + "@jridgewell/sourcemap-codec": "^1.4.10" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.0.7.tgz", + "integrity": "sha512-8cXDaBBHOr2pQ7j77Y6Vp5VDT2sIqWyWQ56TjEq4ih/a4iST3dItRe8Q9fp0rrIl9DoKhWQtUQz/YpOxLkXbNA==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/set-array": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.1.tgz", + "integrity": "sha512-Ct5MqZkLGEXTVmQYbGtx9SVqD2fqwvdubdps5D3djjAkgkKwT918VNOz65pEHFaYTeWcukmJmH5SwsA9Tn2ObQ==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.4.13", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.13.tgz", + "integrity": "sha512-GryiOJmNcWbovBxTfZSF71V/mXbgcV3MewDe3kIMCLyIh5e7SKAeUZs+rMnJ8jkMolZ/4/VsdBmMrw3l+VdZ3w==", + "dev": true + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.13.tgz", + "integrity": "sha512-o1xbKhp9qnIAoHJSWd6KlCZfqslL4valSF81H8ImioOAxluWYWOpWkpyktY2vnt4tbrX9XYaxovq6cgowaJp2w==", + "dev": true, + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "node_modules/@lezer/common": { + "version": "0.15.12", + "resolved": "https://registry.npmjs.org/@lezer/common/-/common-0.15.12.tgz", + "integrity": "sha512-edfwCxNLnzq5pBA/yaIhwJ3U3Kz8VAUOTRg0hhxaizaI1N+qxV7EXDv/kLCkLeq2RzSFvxexlaj5Mzfn2kY0Ig==" + }, + "node_modules/@lezer/cpp": { + "version": "0.15.3", + "resolved": "https://registry.npmjs.org/@lezer/cpp/-/cpp-0.15.3.tgz", + "integrity": "sha512-QE5YxhnoQ4eJH9G2h5r+m4Zq7d/0NmA0eAnZmiOVggI7a3jpODIXZeJbkUPf4U2yzNCSWAGpZVk8XxkA+cTZvA==", + "dependencies": { + "@lezer/lr": "^0.15.0" + } + }, + "node_modules/@lezer/css": { + "version": "0.15.2", + "resolved": "https://registry.npmjs.org/@lezer/css/-/css-0.15.2.tgz", + "integrity": "sha512-tnMOMZY0Zs6JQeVjqfmREYMV0GnmZR1NitndLWioZMD6mA7VQF/PPKPmJX1f+ZgVZQc5Am0df9mX3aiJnNJlKQ==", + "dependencies": { + "@lezer/lr": "^0.15.0" + } + }, + "node_modules/@lezer/html": { + "version": "0.15.1", + "resolved": "https://registry.npmjs.org/@lezer/html/-/html-0.15.1.tgz", + "integrity": "sha512-0ZYVhu+RwN6ZMM0gNnTxenRAdoycKc2wvpLfMjP0JkKR0vMxhtuLaIpsq9KW2Mv6l7ux5vdjq8CQ7fKDvia8KA==", + "dependencies": { + "@lezer/lr": "^0.15.0" + } + }, + "node_modules/@lezer/java": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/@lezer/java/-/java-0.15.0.tgz", + "integrity": "sha512-Od2Ugo93XjLxCIEKlrwJfacmSMd7lEnkVQgBjMsZofjwEKZ2Y2ue6URntMFFiftTlNXbE29vYbweWYluEq+Cdw==", + "dependencies": { + "@lezer/lr": "^0.15.0" + } + }, + "node_modules/@lezer/javascript": { + "version": "0.15.3", + "resolved": "https://registry.npmjs.org/@lezer/javascript/-/javascript-0.15.3.tgz", + "integrity": "sha512-8jA2NpOfpWwSPZxRhd9BxK2ZPvGd7nLE3LFTJ5AbMhXAzMHeMjneV6GEVd7dAIee85dtap0jdb6bgOSO0+lfwA==", + "dependencies": { + "@lezer/lr": "^0.15.0" + } + }, + "node_modules/@lezer/json": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/@lezer/json/-/json-0.15.0.tgz", + "integrity": "sha512-OsMjjBkTkeQ15iMCu5U1OiBubRC4V9Wm03zdIlUgNZ20aUPx5DWDRqUc5wG41JXVSj7Lxmo+idlFCfBBdxB8sw==", + "dependencies": { + "@lezer/lr": "^0.15.0" + } + }, + "node_modules/@lezer/lr": { + "version": "0.15.8", + "resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-0.15.8.tgz", + "integrity": "sha512-bM6oE6VQZ6hIFxDNKk8bKPa14hqFrV07J/vHGOeiAbJReIaQXmkVb6xQu4MR+JBTLa5arGRyAAjJe1qaQt3Uvg==", + "dependencies": { + "@lezer/common": "^0.15.0" + } + }, + "node_modules/@lezer/markdown": { + "version": "0.15.6", + "resolved": "https://registry.npmjs.org/@lezer/markdown/-/markdown-0.15.6.tgz", + "integrity": "sha512-1XXLa4q0ZthryUEfO47ipvZHxNb+sCKoQIMM9dKs5vXZOBbgF2Vah/GL3g26BFIAEc2uCv4VQnI+lSrv58BT3g==", + "dependencies": { + "@lezer/common": "^0.15.0" + } + }, + "node_modules/@lezer/php": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/@lezer/php/-/php-0.15.0.tgz", + "integrity": "sha512-kU3QSOko0jsv3RLhABPrRD4wEhaWYh2Uh0lTj9Q9BOsBJ5SoADfifO4gHkEDav7AgL/j+ulkKiHiilciTa/RaQ==", + "dependencies": { + "@lezer/lr": "^0.15.0" + } + }, + "node_modules/@lezer/python": { + "version": "0.15.1", + "resolved": "https://registry.npmjs.org/@lezer/python/-/python-0.15.1.tgz", + "integrity": "sha512-Xdb2nh+FoxR8ssEADGsroDtsnP+EDhiPpW9zhER3h+6cpGtZ2e9Oq/Rwn9nFQRiKCfMT+AQaqC3ZgAbhbnumyQ==", + "dependencies": { + "@lezer/lr": "^0.15.0" + } + }, + "node_modules/@lezer/rust": { + "version": "0.15.1", + "resolved": "https://registry.npmjs.org/@lezer/rust/-/rust-0.15.1.tgz", + "integrity": "sha512-9R7Mcfe/XWodpT7bYNKoOmEAN+AOHHfma9QUTdEhqduzd1G4qsdQkGSMPfsqt24sZCkQ1EREbE/lmEp4YxTlcA==", + "dependencies": { + "@lezer/lr": "^0.15.0" + } + }, + "node_modules/@lezer/xml": { + "version": "0.15.1", + "resolved": "https://registry.npmjs.org/@lezer/xml/-/xml-0.15.1.tgz", + "integrity": "sha512-vVh01enxM9hSGOcFtztmX+Pa460HDq5jIeft9bDCe17PUOU0nAbfo883I3cW9lUOcmWNQ3btbkmXMGjRszJE6g==", + "dependencies": { + "@lezer/lr": "^0.15.0" + } + }, + "node_modules/@maif/react-forms": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/@maif/react-forms/-/react-forms-1.6.3.tgz", + "integrity": "sha512-dMEuffnbPS7m7YM0bJVGajmpTSdHzrngdF8pM/RIPst//X0hjD/1HWveG9qAW5/MC2JIKlBrN7w6IpOdJmTBIg==", + "dependencies": { + "@codemirror/basic-setup": "^0.19.1", + "@codemirror/lang-html": "^0.19.4", + "@codemirror/lang-javascript": "^0.19.7", + "@codemirror/lang-json": "^0.19.2", + "@codemirror/lang-markdown": "^0.19.6", + "@codemirror/language-data": "^0.19.2", + "@codemirror/theme-one-dark": "^0.19.1", + "@emotion/react": "^11.9.3", + "@emotion/styled": "^11.9.3", + "@fortawesome/fontawesome-free": "^5.15.3", + "@hookform/resolvers": "2.4.0", + "@mui/material": "^5.8.7", + "@mui/system": "^5.8.7", + "@mui/x-date-pickers": "^5.0.0-alpha.7", + "classnames": "2.3.0", + "date-fns": "^2.28.0", + "fast-deep-equal": "^3.1.3", + "highlight.js": "^11.5.1", + "lodash.debounce": "4.0.8", + "moment": "2.29.4", + "object-hash": "3.0.0", + "react-feather": "2.0.9", + "react-hook-form": "^7.33.1", + "react-select": "5.2.1", + "react-tooltip": "4.2.21", + "showdown": "1.9.1", + "uuid": "8.3.2", + "yup": "0.32.11" + }, + "peerDependencies": { + "react": "^17.0.2 || 18" + } + }, + "node_modules/@maif/react-forms/node_modules/classnames": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.3.0.tgz", + "integrity": "sha512-UUf/S3eeczXBjHPpSnrZ1ZyxH3KmLW8nVYFUWIZA/dixYMIQr7l94yYKxaAkmPk7HO9dlT6gFqAPZC02tTdfQw==" + }, + "node_modules/@maif/react-forms/node_modules/react-dom": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-17.0.2.tgz", + "integrity": "sha512-s4h96KtLDUQlsENhMn1ar8t2bEa+q/YAtj8pPPdIjPDGBDIVNsrD9aXNWqspUe6AzKCIG0C1HZZLqLV7qpOBGA==", + "peer": true, + "dependencies": { + "loose-envify": "^1.1.0", + "object-assign": "^4.1.1", + "scheduler": "^0.20.2" + }, + "peerDependencies": { + "react": "17.0.2" + } + }, + "node_modules/@maif/react-forms/node_modules/react-feather": { + "version": "2.0.9", + "resolved": "https://registry.npmjs.org/react-feather/-/react-feather-2.0.9.tgz", + "integrity": "sha512-yMfCGRkZdXwIs23Zw/zIWCJO3m3tlaUvtHiXlW+3FH7cIT6fiK1iJ7RJWugXq7Fso8ZaQyUm92/GOOHXvkiVUw==", + "dependencies": { + "prop-types": "^15.7.2" + }, + "peerDependencies": { + "react": "^16.8.6 || ^17" + } + }, + "node_modules/@maif/react-forms/node_modules/react-select": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/react-select/-/react-select-5.2.1.tgz", + "integrity": "sha512-OOyNzfKrhOcw/BlembyGWgdlJ2ObZRaqmQppPFut1RptJO423j+Y+JIsmxkvsZ4D/3CpOmwIlCvWbbAWEdh12A==", + "dependencies": { + "@babel/runtime": "^7.12.0", + "@emotion/cache": "^11.4.0", + "@emotion/react": "^11.1.1", + "@types/react-transition-group": "^4.4.0", + "memoize-one": "^5.0.0", + "prop-types": "^15.6.0", + "react-transition-group": "^4.3.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0", + "react-dom": "^16.8.0 || ^17.0.0" + } + }, + "node_modules/@maif/react-forms/node_modules/react-tooltip": { + "version": "4.2.21", + "resolved": "https://registry.npmjs.org/react-tooltip/-/react-tooltip-4.2.21.tgz", + "integrity": "sha512-zSLprMymBDowknr0KVDiJ05IjZn9mQhhg4PRsqln0OZtURAJ1snt1xi5daZfagsh6vfsziZrc9pErPTDY1ACig==", + "dependencies": { + "prop-types": "^15.7.2", + "uuid": "^7.0.3" + }, + "engines": { + "npm": ">=6.13" + }, + "peerDependencies": { + "react": ">=16.0.0", + "react-dom": ">=16.0.0" + } + }, + "node_modules/@maif/react-forms/node_modules/react-tooltip/node_modules/uuid": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-7.0.3.tgz", + "integrity": "sha512-DPSke0pXhTZgoF/d+WSt2QaKMCFSfx7QegxEWT+JOuHF5aWrKEn0G+ztjuJg/gG8/ItK+rbPCD/yNv8yyih6Cg==", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/@maif/react-forms/node_modules/scheduler": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.20.2.tgz", + "integrity": "sha512-2eWfGgAqqWFGqtdMmcL5zCMK1U8KlXv8SQFGglL3CEtd0aDVDWgeF/YoCmvln55m5zSk3J/20hTaSBeSObsQDQ==", + "peer": true, + "dependencies": { + "loose-envify": "^1.1.0", + "object-assign": "^4.1.1" + } + }, + "node_modules/@mui/base": { + "version": "5.0.0-beta.5", + "resolved": "https://registry.npmjs.org/@mui/base/-/base-5.0.0-beta.5.tgz", + "integrity": "sha512-vy3TWLQYdGNecTaufR4wDNQFV2WEg6wRPi6BVbx6q1vP3K1mbxIn1+XOqOzfYBXjFHvMx0gZAo2TgWbaqfgvAA==", + "dependencies": { + "@babel/runtime": "^7.22.5", + "@emotion/is-prop-valid": "^1.2.1", + "@mui/types": "^7.2.4", + "@mui/utils": "^5.13.6", + "@popperjs/core": "^2.11.8", + "clsx": "^1.2.1", + "prop-types": "^15.8.1", + "react-is": "^18.2.0" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui" + }, + "peerDependencies": { + "@types/react": "^17.0.0 || ^18.0.0", + "react": "^17.0.0 || ^18.0.0", + "react-dom": "^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/base/node_modules/react-is": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", + "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==" + }, + "node_modules/@mui/core-downloads-tracker": { + "version": "5.13.4", + "resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-5.13.4.tgz", + "integrity": "sha512-yFrMWcrlI0TqRN5jpb6Ma9iI7sGTHpytdzzL33oskFHNQ8UgrtPas33Y1K7sWAMwCrr1qbWDrOHLAQG4tAzuSw==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui" + } + }, + "node_modules/@mui/material": { + "version": "5.13.6", + "resolved": "https://registry.npmjs.org/@mui/material/-/material-5.13.6.tgz", + "integrity": "sha512-/c2ZApeQm2sTYdQXjqEnldaBMBcUEiyu2VRS6bS39ZeNaAcCLBQbYocLR46R+f0S5dgpBzB0T4AsOABPOFYZ5Q==", + "dependencies": { + "@babel/runtime": "^7.22.5", + "@mui/base": "5.0.0-beta.5", + "@mui/core-downloads-tracker": "^5.13.4", + "@mui/system": "^5.13.6", + "@mui/types": "^7.2.4", + "@mui/utils": "^5.13.6", + "@types/react-transition-group": "^4.4.6", + "clsx": "^1.2.1", + "csstype": "^3.1.2", + "prop-types": "^15.8.1", + "react-is": "^18.2.0", + "react-transition-group": "^4.4.5" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui" + }, + "peerDependencies": { + "@emotion/react": "^11.5.0", + "@emotion/styled": "^11.3.0", + "@types/react": "^17.0.0 || ^18.0.0", + "react": "^17.0.0 || ^18.0.0", + "react-dom": "^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@emotion/react": { + "optional": true + }, + "@emotion/styled": { + "optional": true + }, + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/material/node_modules/react-is": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", + "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==" + }, + "node_modules/@mui/private-theming": { + "version": "5.13.1", + "resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-5.13.1.tgz", + "integrity": "sha512-HW4npLUD9BAkVppOUZHeO1FOKUJWAwbpy0VQoGe3McUYTlck1HezGHQCfBQ5S/Nszi7EViqiimECVl9xi+/WjQ==", + "dependencies": { + "@babel/runtime": "^7.21.0", + "@mui/utils": "^5.13.1", + "prop-types": "^15.8.1" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui" + }, + "peerDependencies": { + "@types/react": "^17.0.0 || ^18.0.0", + "react": "^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/styled-engine": { + "version": "5.13.2", + "resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-5.13.2.tgz", + "integrity": "sha512-VCYCU6xVtXOrIN8lcbuPmoG+u7FYuOERG++fpY74hPpEWkyFQG97F+/XfTQVYzlR2m7nPjnwVUgATcTCMEaMvw==", + "dependencies": { + "@babel/runtime": "^7.21.0", + "@emotion/cache": "^11.11.0", + "csstype": "^3.1.2", + "prop-types": "^15.8.1" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui" + }, + "peerDependencies": { + "@emotion/react": "^11.4.1", + "@emotion/styled": "^11.3.0", + "react": "^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@emotion/react": { + "optional": true + }, + "@emotion/styled": { + "optional": true + } + } + }, + "node_modules/@mui/system": { + "version": "5.13.6", + "resolved": "https://registry.npmjs.org/@mui/system/-/system-5.13.6.tgz", + "integrity": "sha512-G3Xr28uLqU3DyF6r2LQkHGw/ku4P0AHzlKVe7FGXOPl7X1u+hoe2xxj8Vdiq/69II/mh9OP21i38yBWgWb7WgQ==", + "dependencies": { + "@babel/runtime": "^7.22.5", + "@mui/private-theming": "^5.13.1", + "@mui/styled-engine": "^5.13.2", + "@mui/types": "^7.2.4", + "@mui/utils": "^5.13.6", + "clsx": "^1.2.1", + "csstype": "^3.1.2", + "prop-types": "^15.8.1" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui" + }, + "peerDependencies": { + "@emotion/react": "^11.5.0", + "@emotion/styled": "^11.3.0", + "@types/react": "^17.0.0 || ^18.0.0", + "react": "^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@emotion/react": { + "optional": true + }, + "@emotion/styled": { + "optional": true + }, + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/types": { + "version": "7.2.4", + "resolved": "https://registry.npmjs.org/@mui/types/-/types-7.2.4.tgz", + "integrity": "sha512-LBcwa8rN84bKF+f5sDyku42w1NTxaPgPyYKODsh01U1fVstTClbUoSA96oyRBnSNyEiAVjKm6Gwx9vjR+xyqHA==", + "peerDependencies": { + "@types/react": "*" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/utils": { + "version": "5.13.6", + "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-5.13.6.tgz", + "integrity": "sha512-ggNlxl5NPSbp+kNcQLmSig6WVB0Id+4gOxhx644987v4fsji+CSXc+MFYLocFB/x4oHtzCUlSzbVHlJfP/fXoQ==", + "dependencies": { + "@babel/runtime": "^7.22.5", + "@types/prop-types": "^15.7.5", + "@types/react-is": "^18.2.0", + "prop-types": "^15.8.1", + "react-is": "^18.2.0" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui" + }, + "peerDependencies": { + "react": "^17.0.0 || ^18.0.0" + } + }, + "node_modules/@mui/utils/node_modules/react-is": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", + "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==" + }, + "node_modules/@mui/x-date-pickers": { + "version": "5.0.0-beta.0", + "resolved": "https://registry.npmjs.org/@mui/x-date-pickers/-/x-date-pickers-5.0.0-beta.0.tgz", + "integrity": "sha512-WfcYe+5j3xbGO9d+uMFem06b9q+9yIcFj0dP3PKCa1zb6m3Tbkigig6vlCuHLKLSXe1P6IQCt+BNVVbU1rfh7A==", + "dependencies": { + "@babel/runtime": "^7.17.2", + "@date-io/core": "^2.14.0", + "@date-io/date-fns": "^2.14.0", + "@date-io/dayjs": "^2.14.0", + "@date-io/luxon": "^2.14.0", + "@date-io/moment": "^2.14.0", + "@mui/utils": "^5.4.1", + "@types/react-transition-group": "^4.4.4", + "clsx": "^1.2.1", + "prop-types": "^15.7.2", + "react-transition-group": "^4.4.2", + "rifm": "^0.12.1" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui" + }, + "peerDependencies": { + "@emotion/react": "^11.9.0", + "@emotion/styled": "^11.8.1", + "@mui/material": "^5.4.1", + "@mui/system": "^5.4.1", + "date-fns": "^2.25.0", + "dayjs": "^1.10.7", + "luxon": "^1.28.0 || ^2.0.0", + "moment": "^2.29.1", + "react": "^17.0.2 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@emotion/react": { + "optional": true + }, + "@emotion/styled": { + "optional": true + }, + "date-fns": { + "optional": true + }, + "dayjs": { + "optional": true + }, + "luxon": { + "optional": true + }, + "moment": { + "optional": true + } + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@playwright/test": { + "version": "1.41.1", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.41.1.tgz", + "integrity": "sha512-9g8EWTjiQ9yFBXc6HjCWe41msLpxEX0KhmfmPl9RPLJdfzL4F0lg2BdJ91O9azFdl11y1pmpwdjBiSxvqc+btw==", + "dev": true, + "dependencies": { + "playwright": "1.41.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/@popperjs/core": { + "version": "2.11.8", + "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", + "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/popperjs" + } + }, + "node_modules/@remix-run/router": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.0.0.tgz", + "integrity": "sha512-SCR1cxRSMNKjaVYptCzBApPDqGwa3FGdjVHc+rOToocNPHQdIYLZBfv/3f+KvYuXDkUGVIW9IAzmPNZDRL1I4A==", + "engines": { + "node": ">=14" + } + }, + "node_modules/@rollup/pluginutils": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-4.2.1.tgz", + "integrity": "sha512-iKnFXr7NkdZAIHiIWE+BX5ULi/ucVFYWD6TbAV+rZctiRTY2PL6tsIKhoIOaoskiWAkgu+VsbXgUVDNLHf+InQ==", + "dev": true, + "dependencies": { + "estree-walker": "^2.0.1", + "picomatch": "^2.2.2" + }, + "engines": { + "node": ">= 8.0.0" + } + }, + "node_modules/@tanstack/react-table": { + "version": "8.1.3", + "resolved": "https://registry.npmjs.org/@tanstack/react-table/-/react-table-8.1.3.tgz", + "integrity": "sha512-rgGb4Sou8kuJI2NuJbDSS/wRc+TVmXZPg5+vslHZqA+tLvHvYgLHndBc6kW2fzCdInBshJEgHAnDXillYGYi+w==", + "dependencies": { + "@tanstack/table-core": "8.1.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": ">=16", + "react-dom": ">=16" + } + }, + "node_modules/@tanstack/table-core": { + "version": "8.1.2", + "resolved": "https://registry.npmjs.org/@tanstack/table-core/-/table-core-8.1.2.tgz", + "integrity": "sha512-h0e9xBC0BRVoQE8w5BVypjPc2x5+H1VcwQDLKdijoVgUpO2S0ixjY9ejZ3YAtPYkBZTukLm9+3wfF4CFUXwD/Q==", + "engines": { + "node": ">=12" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@textea/json-viewer": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@textea/json-viewer/-/json-viewer-3.1.1.tgz", + "integrity": "sha512-eaYwufnvQhlbdeWdqhERdp4yOKp2bILiMOGKIRBFoFpg3ry6L06GcdNFffe15C8T+2F/vUUEtLVpZ9i5f8NUrw==", + "dependencies": { + "clsx": "^1.2.1", + "copy-to-clipboard": "^3.3.3", + "zustand": "^4.3.7" + }, + "peerDependencies": { + "@emotion/react": "^11", + "@emotion/styled": "^11", + "@mui/material": "^5", + "react": "^17 || ^18", + "react-dom": "^17 || ^18" + } + }, + "node_modules/@types/istanbul-lib-coverage": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.4.tgz", + "integrity": "sha512-z/QT1XN4K4KYuslS23k62yDIDLwLFkzxOuMplDtObz0+y7VqJCaO2o+SPwHCvLFZh7xazvvoor2tA/hPz9ee7g==", + "dev": true + }, + "node_modules/@types/istanbul-lib-report": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz", + "integrity": "sha512-plGgXAPfVKFoYfa9NpYDAkseG+g6Jr294RqeqcqDixSbU34MZVJRi/P+7Y8GDpzkEwLaGZZOpKIEmeVZNtKsrg==", + "dev": true, + "dependencies": { + "@types/istanbul-lib-coverage": "*" + } + }, + "node_modules/@types/istanbul-reports": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.1.tgz", + "integrity": "sha512-c3mAZEuK0lvBp8tmuL74XRKn1+y2dcwOUpH7x4WrF6gk1GIgiluDRgMYQtw2OFcBvAJWlt6ASU3tSqxp0Uu0Aw==", + "dev": true, + "dependencies": { + "@types/istanbul-lib-report": "*" + } + }, + "node_modules/@types/jest": { + "version": "26.0.24", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-26.0.24.tgz", + "integrity": "sha512-E/X5Vib8BWqZNRlDxj9vYXhsDwPYbPINqKF9BsnSoon4RQ0D9moEuLD8txgyypFLH7J4+Lho9Nr/c8H0Fi+17w==", + "dev": true, + "dependencies": { + "jest-diff": "^26.0.0", + "pretty-format": "^26.0.0" + } + }, + "node_modules/@types/json-schema": { + "version": "7.0.11", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.11.tgz", + "integrity": "sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==", + "dev": true + }, + "node_modules/@types/lodash": { + "version": "4.14.182", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.182.tgz", + "integrity": "sha512-/THyiqyQAP9AfARo4pF+aCGcyiQ94tX/Is2I7HofNRqoYLgN1PBoOWu2/zTA5zMxzP5EFutMtWtGAFRKUe961Q==" + }, + "node_modules/@types/node": { + "version": "16.11.41", + "resolved": "https://registry.npmjs.org/@types/node/-/node-16.11.41.tgz", + "integrity": "sha512-mqoYK2TnVjdkGk8qXAVGc/x9nSaTpSrFaGFm43BUH3IdoBV0nta6hYaGmdOvIMlbHJbUEVen3gvwpwovAZKNdQ==", + "dev": true + }, + "node_modules/@types/parse-json": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.0.tgz", + "integrity": "sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==" + }, + "node_modules/@types/prop-types": { + "version": "15.7.5", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.5.tgz", + "integrity": "sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==" + }, + "node_modules/@types/react": { + "version": "18.0.13", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.0.13.tgz", + "integrity": "sha512-psqptIYQxGUFuGYwP3KCFVtPTkMpIcrqFmtKblWEUQhLuYLpHBwJkXhjp6eHfDM5IbyskY4x7qQpLedEsPkHlA==", + "dependencies": { + "@types/prop-types": "*", + "@types/scheduler": "*", + "csstype": "^3.0.2" + } + }, + "node_modules/@types/react-dom": { + "version": "18.0.5", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.0.5.tgz", + "integrity": "sha512-OWPWTUrY/NIrjsAPkAk1wW9LZeIjSvkXRhclsFO8CZcZGCOg2G0YZy4ft+rOyYxy8B7ui5iZzi9OkDebZ7/QSA==", + "dev": true, + "dependencies": { + "@types/react": "*" + } + }, + "node_modules/@types/react-is": { + "version": "18.2.1", + "resolved": "https://registry.npmjs.org/@types/react-is/-/react-is-18.2.1.tgz", + "integrity": "sha512-wyUkmaaSZEzFZivD8F2ftSyAfk6L+DfFliVj/mYdOXbVjRcS87fQJLTnhk6dRZPuJjI+9g6RZJO4PNCngUrmyw==", + "dependencies": { + "@types/react": "*" + } + }, + "node_modules/@types/react-transition-group": { + "version": "4.4.6", + "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.6.tgz", + "integrity": "sha512-VnCdSxfcm08KjsJVQcfBmhEQAPnLB8G08hAxn39azX1qYBQ/5RVQuoHuKIcfKOdncuaUvEpFKFzEvbtIMsfVew==", + "dependencies": { + "@types/react": "*" + } + }, + "node_modules/@types/scheduler": { + "version": "0.16.2", + "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.2.tgz", + "integrity": "sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew==" + }, + "node_modules/@types/yargs": { + "version": "15.0.14", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-15.0.14.tgz", + "integrity": "sha512-yEJzHoxf6SyQGhBhIYGXQDSCkJjB6HohDShto7m8vaKg9Yp0Yn8+71J9eakh2bnPg6BfsH9PRMhiRTZnd4eXGQ==", + "dev": true, + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/@types/yargs-parser": { + "version": "21.0.0", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.0.tgz", + "integrity": "sha512-iO9ZQHkZxHn4mSakYV0vFHAVDyEOIJQrV2uZ06HxEPcx+mt8swXoZHIbaaJ2crJYFfErySgktuTZ3BeLz+XmFA==", + "dev": true + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "5.28.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.28.0.tgz", + "integrity": "sha512-DXVU6Cg29H2M6EybqSg2A+x8DgO9TCUBRp4QEXQHJceLS7ogVDP0g3Lkg/SZCqcvkAP/RruuQqK0gdlkgmhSUA==", + "dev": true, + "dependencies": { + "@typescript-eslint/scope-manager": "5.28.0", + "@typescript-eslint/type-utils": "5.28.0", + "@typescript-eslint/utils": "5.28.0", + "debug": "^4.3.4", + "functional-red-black-tree": "^1.0.1", + "ignore": "^5.2.0", + "regexpp": "^3.2.0", + "semver": "^7.3.7", + "tsutils": "^3.21.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^5.0.0", + "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "5.28.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.28.0.tgz", + "integrity": "sha512-ekqoNRNK1lAcKhZESN/PdpVsWbP9jtiNqzFWkp/yAUdZvJalw2heCYuqRmM5eUJSIYEkgq5sGOjq+ZqsLMjtRA==", + "dev": true, + "dependencies": { + "@typescript-eslint/scope-manager": "5.28.0", + "@typescript-eslint/types": "5.28.0", + "@typescript-eslint/typescript-estree": "5.28.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "5.28.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.28.0.tgz", + "integrity": "sha512-LeBLTqF/he1Z+boRhSqnso6YrzcKMTQ8bO/YKEe+6+O/JGof9M0g3IJlIsqfrK/6K03MlFIlycbf1uQR1IjE+w==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "5.28.0", + "@typescript-eslint/visitor-keys": "5.28.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "5.28.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.28.0.tgz", + "integrity": "sha512-SyKjKh4CXPglueyC6ceAFytjYWMoPHMswPQae236zqe1YbhvCVQyIawesYywGiu98L9DwrxsBN69vGIVxJ4mQQ==", + "dev": true, + "dependencies": { + "@typescript-eslint/utils": "5.28.0", + "debug": "^4.3.4", + "tsutils": "^3.21.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/types": { + "version": "5.28.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.28.0.tgz", + "integrity": "sha512-2OOm8ZTOQxqkPbf+DAo8oc16sDlVR5owgJfKheBkxBKg1vAfw2JsSofH9+16VPlN9PWtv8Wzhklkqw3k/zCVxA==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "5.28.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.28.0.tgz", + "integrity": "sha512-9GX+GfpV+F4hdTtYc6OV9ZkyYilGXPmQpm6AThInpBmKJEyRSIjORJd1G9+bknb7OTFYL+Vd4FBJAO6T78OVqA==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "5.28.0", + "@typescript-eslint/visitor-keys": "5.28.0", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "semver": "^7.3.7", + "tsutils": "^3.21.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "5.28.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.28.0.tgz", + "integrity": "sha512-E60N5L0fjv7iPJV3UGc4EC+A3Lcj4jle9zzR0gW7vXhflO7/J29kwiTGITA2RlrmPokKiZbBy2DgaclCaEUs6g==", + "dev": true, + "dependencies": { + "@types/json-schema": "^7.0.9", + "@typescript-eslint/scope-manager": "5.28.0", + "@typescript-eslint/types": "5.28.0", + "@typescript-eslint/typescript-estree": "5.28.0", + "eslint-scope": "^5.1.1", + "eslint-utils": "^3.0.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "5.28.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.28.0.tgz", + "integrity": "sha512-BtfP1vCor8cWacovzzPFOoeW4kBQxzmhxGoOpt0v1SFvG+nJ0cWaVdJk7cky1ArTcFHHKNIxyo2LLr3oNkSuXA==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "5.28.0", + "eslint-visitor-keys": "^3.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-1.3.2.tgz", + "integrity": "sha512-aurBNmMo0kz1O4qRoY+FM4epSA39y3ShWGuqfLRA/3z0oEJAdtoSfgA3aO98/PCCHAqMaduLxIxErWrVKIFzXA==", + "dev": true, + "dependencies": { + "@babel/core": "^7.17.10", + "@babel/plugin-transform-react-jsx": "^7.17.3", + "@babel/plugin-transform-react-jsx-development": "^7.16.7", + "@babel/plugin-transform-react-jsx-self": "^7.16.7", + "@babel/plugin-transform-react-jsx-source": "^7.16.7", + "@rollup/pluginutils": "^4.2.1", + "react-refresh": "^0.13.0", + "resolve": "^1.22.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/@xstate/react": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@xstate/react/-/react-3.0.0.tgz", + "integrity": "sha512-KHSCfwtb8gZ7QH2luihvmKYI+0lcdHQOmGNRUxUEs4zVgaJCyd8csCEmwPsudpliLdUmyxX2pzUBojFkINpotw==", + "dependencies": { + "use-isomorphic-layout-effect": "^1.0.0", + "use-sync-external-store": "^1.0.0" + }, + "peerDependencies": { + "@xstate/fsm": "^2.0.0", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "xstate": "^4.31.0" + }, + "peerDependenciesMeta": { + "@xstate/fsm": { + "optional": true + }, + "xstate": { + "optional": true + } + } + }, + "node_modules/acorn": { + "version": "8.7.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.7.1.tgz", + "integrity": "sha512-Xx54uLJQZ19lKygFXOWsscKUbsBZW0CPykPhVQdhIeIwrbPmJzqeASDInc8nKBnp/JT6igTs82qPXz069H8I/A==", + "dev": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "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==", + "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/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "node_modules/array-includes": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.5.tgz", + "integrity": "sha512-iSDYZMMyTPkiFasVqfuAQnWAYcvO/SeBSCGKePoEthjp4LEMTe4uLc7b025o4jAZpHhihh8xPo99TNWUWWkGDQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "es-abstract": "^1.19.5", + "get-intrinsic": "^1.1.1", + "is-string": "^1.0.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/array.prototype.flatmap": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.0.tgz", + "integrity": "sha512-PZC9/8TKAIxcWKdyeb77EzULHPrIX/tIZebLJUQOMR1OwYosT8yggdfWScfTBCDj5utONvOuPQQumYsU2ULbkg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3", + "es-abstract": "^1.19.2", + "es-shim-unscopables": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/babel-plugin-macros": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/babel-plugin-macros/-/babel-plugin-macros-3.1.0.tgz", + "integrity": "sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==", + "dependencies": { + "@babel/runtime": "^7.12.5", + "cosmiconfig": "^7.0.0", + "resolve": "^1.19.0" + }, + "engines": { + "node": ">=10", + "npm": ">=6" + } + }, + "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==" + }, + "node_modules/big-integer": { + "version": "1.6.51", + "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.51.tgz", + "integrity": "sha512-GPEid2Y9QU1Exl1rpO9B2IPJGHPSupF5GnVIP0blYvNOMer2bTvSWs1jGOUg04hTmu67nmLsQ9TBo1puaotBHg==", + "engines": { + "node": ">=0.6" + } + }, + "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==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/bootstrap" + }, + "peerDependencies": { + "@popperjs/core": "^2.10.2" + } + }, + "node_modules/bootstrap-icons": { + "version": "1.8.3", + "resolved": "https://registry.npmjs.org/bootstrap-icons/-/bootstrap-icons-1.8.3.tgz", + "integrity": "sha512-s5kmttnbq4BXbx3Bwnj39y+t7Vc3blTtyD77W3aYQ1LlNoS3lNbbGvSYhIbg26Im8KmjScyFpHEevlPOBcIDdA==" + }, + "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==", + "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/broadcast-channel": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/broadcast-channel/-/broadcast-channel-3.7.0.tgz", + "integrity": "sha512-cIAKJXAxGJceNZGTZSBzMxzyOn72cVgPnKx4dc6LRjQgbaJUQqhy5rzL3zbMxkMWsGKkv2hSFkPRMEXfoMZ2Mg==", + "dependencies": { + "@babel/runtime": "^7.7.2", + "detect-node": "^2.1.0", + "js-sha3": "0.8.0", + "microseconds": "0.2.0", + "nano-time": "1.0.0", + "oblivious-set": "1.0.0", + "rimraf": "3.0.2", + "unload": "2.2.0" + } + }, + "node_modules/browserslist": { + "version": "4.20.4", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.20.4.tgz", + "integrity": "sha512-ok1d+1WpnU24XYN7oC3QWgTyMhY/avPJ/r9T00xxvUOIparA/gc+UPUMaod3i+G6s+nI2nUb9xZ5k794uIwShw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + } + ], + "dependencies": { + "caniuse-lite": "^1.0.30001349", + "electron-to-chromium": "^1.4.147", + "escalade": "^3.1.1", + "node-releases": "^2.0.5", + "picocolors": "^1.0.0" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/buffer-writer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/buffer-writer/-/buffer-writer-2.0.0.tgz", + "integrity": "sha512-a7ZpuTZU1TRtnwyCNW3I5dc0wWNC3VR9S++Ewyk2HHZdrO3CQJqSpd+95Us590V6AL7JqUAH2IwZ/398PmNFgw==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/call-bind": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", + "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", + "dev": true, + "dependencies": { + "function-bind": "^1.1.1", + "get-intrinsic": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001355", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001355.tgz", + "integrity": "sha512-Sd6pjJHF27LzCB7pT7qs+kuX2ndurzCzkpJl6Qct7LPSZ9jn0bkOA8mdgMgmqnQAWLVOOGjLpc+66V57eLtb1g==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + } + ] + }, + "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==", + "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.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", + "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + ], + "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/chokidar/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/classnames": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.3.1.tgz", + "integrity": "sha512-OlQdbZ7gLfGarSqxesMesDa5uz7KFbID8Kpq/SxIoNGDqY8lSYs0D+hhtBXhcdB3rcbXArFr7vlHheLk1voeNA==" + }, + "node_modules/cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "dev": true, + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, + "node_modules/clsx": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz", + "integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==", + "engines": { + "node": ">=6" + } + }, + "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==", + "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": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==" + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" + }, + "node_modules/convert-source-map": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.8.0.tgz", + "integrity": "sha512-+OQdjP49zViI/6i7nIJpA8rAl4sV/JdPfU9nZs3VqOwGIgizICvuN2ru6fMd+4llL0tar18UYJXfZ/TWtmhUjA==", + "dependencies": { + "safe-buffer": "~5.1.1" + } + }, + "node_modules/copy-to-clipboard": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/copy-to-clipboard/-/copy-to-clipboard-3.3.3.tgz", + "integrity": "sha512-2KV8NhB5JqC3ky0r9PMCAZKbUHSwtEo4CwCs0KXgruG43gX5PMqDEBbVU4OUzw2MuAWUfsuFmWvEKG5QRfSnJA==", + "dependencies": { + "toggle-selection": "^1.0.6" + } + }, + "node_modules/cosmiconfig": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz", + "integrity": "sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==", + "dependencies": { + "@types/parse-json": "^4.0.0", + "import-fresh": "^3.2.1", + "parse-json": "^5.0.0", + "path-type": "^4.0.0", + "yaml": "^1.10.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/crelt": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.5.tgz", + "integrity": "sha512-+BO9wPPi+DWTDcNYhr/W90myha8ptzftZT+LwcmUbbok0rcP/fequmFYCw8NMoH7pkAZQzU78b3kYrlua5a9eA==" + }, + "node_modules/cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/csstype": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.2.tgz", + "integrity": "sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==" + }, + "node_modules/date-fns": { + "version": "2.28.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.28.0.tgz", + "integrity": "sha512-8d35hViGYx/QH0icHYCeLmsLmMUheMmTyV9Fcm6gvNwdw31yXXH+O85sOBJ+OLnLQMKZowvpKb6FgMIQjcpvQw==", + "engines": { + "node": ">=0.11" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/date-fns" + } + }, + "node_modules/dayjs": { + "version": "1.11.3", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.3.tgz", + "integrity": "sha512-xxwlswWOlGhzgQ4TKzASQkUhqERI3egRNqgV4ScR8wlANA/A9tZ7miXa44vTTKEq5l7vWoL5G57bG3zA+Kow0A==", + "optional": true, + "peer": true + }, + "node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true + }, + "node_modules/define-lazy-prop": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz", + "integrity": "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/define-properties": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.4.tgz", + "integrity": "sha512-uckOqKcfaVvtBdsVkdPv3XjveQJsNQqmhXgRi8uhvWWuPYZCNlzT8qAyblUgNoXdHdjMTzAqeGjAoli8f+bzPA==", + "dev": true, + "dependencies": { + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/detect-node": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz", + "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==" + }, + "node_modules/diff-sequences": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-26.6.2.tgz", + "integrity": "sha512-Mv/TDa3nZ9sbc5soK+OoA74BsS3mL37yixCvUAQkiuA4Wz6YtwP/K47n2rv2ovzHZvoiQeA5FTQOschKkEwB0Q==", + "dev": true, + "engines": { + "node": ">= 10.14.2" + } + }, + "node_modules/dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, + "dependencies": { + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/dom-helpers": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz", + "integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==", + "dependencies": { + "@babel/runtime": "^7.8.7", + "csstype": "^3.0.2" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.4.158", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.158.tgz", + "integrity": "sha512-gppO3/+Y6sP432HtvwvuU8S+YYYLH4PmAYvQwqUtt9HDOmEsBwQfLnK9T8+1NIKwAS1BEygIjTaATC4H5EzvxQ==", + "dev": true + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "node_modules/error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/es-abstract": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.20.1.tgz", + "integrity": "sha512-WEm2oBhfoI2sImeM4OF2zE2V3BYdSF+KnSi9Sidz51fQHd7+JuF8Xgcj9/0o+OWeIeIS/MiuNnlruQrJf16GQA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "es-to-primitive": "^1.2.1", + "function-bind": "^1.1.1", + "function.prototype.name": "^1.1.5", + "get-intrinsic": "^1.1.1", + "get-symbol-description": "^1.0.0", + "has": "^1.0.3", + "has-property-descriptors": "^1.0.0", + "has-symbols": "^1.0.3", + "internal-slot": "^1.0.3", + "is-callable": "^1.2.4", + "is-negative-zero": "^2.0.2", + "is-regex": "^1.1.4", + "is-shared-array-buffer": "^1.0.2", + "is-string": "^1.0.7", + "is-weakref": "^1.0.2", + "object-inspect": "^1.12.0", + "object-keys": "^1.1.1", + "object.assign": "^4.1.2", + "regexp.prototype.flags": "^1.4.3", + "string.prototype.trimend": "^1.0.5", + "string.prototype.trimstart": "^1.0.5", + "unbox-primitive": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es-shim-unscopables": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.0.0.tgz", + "integrity": "sha512-Jm6GPcCdC30eMLbZ2x8z2WuRwAws3zTBBKuusffYVUrNj/GVSUAZ+xKMaUpfNDR5IbyNA5LJbaecoUVbmUcB1w==", + "dev": true, + "dependencies": { + "has": "^1.0.3" + } + }, + "node_modules/es-to-primitive": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", + "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", + "dev": true, + "dependencies": { + "is-callable": "^1.1.4", + "is-date-object": "^1.0.1", + "is-symbol": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/esbuild": { + "version": "0.14.44", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.14.44.tgz", + "integrity": "sha512-Rn+lRRfj60r/3svI6NgAVnetzp3vMOj17BThuhshSj/gS1LR03xrjkDYyfPmrYG/0c3D68rC6FNYMQ3yRbiXeQ==", + "dev": true, + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "esbuild-android-64": "0.14.44", + "esbuild-android-arm64": "0.14.44", + "esbuild-darwin-64": "0.14.44", + "esbuild-darwin-arm64": "0.14.44", + "esbuild-freebsd-64": "0.14.44", + "esbuild-freebsd-arm64": "0.14.44", + "esbuild-linux-32": "0.14.44", + "esbuild-linux-64": "0.14.44", + "esbuild-linux-arm": "0.14.44", + "esbuild-linux-arm64": "0.14.44", + "esbuild-linux-mips64le": "0.14.44", + "esbuild-linux-ppc64le": "0.14.44", + "esbuild-linux-riscv64": "0.14.44", + "esbuild-linux-s390x": "0.14.44", + "esbuild-netbsd-64": "0.14.44", + "esbuild-openbsd-64": "0.14.44", + "esbuild-sunos-64": "0.14.44", + "esbuild-windows-32": "0.14.44", + "esbuild-windows-64": "0.14.44", + "esbuild-windows-arm64": "0.14.44" + } + }, + "node_modules/esbuild-android-64": { + "version": "0.14.44", + "resolved": "https://registry.npmjs.org/esbuild-android-64/-/esbuild-android-64-0.14.44.tgz", + "integrity": "sha512-dFPHBXmx385zuJULAD/Cmq/LyPRXiAWbf9ylZtY0wJ8iVyWfKYaCYxeJx8OAZUuj46ZwNa7MzW2GBAQLOeiemg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-android-arm64": { + "version": "0.14.44", + "resolved": "https://registry.npmjs.org/esbuild-android-arm64/-/esbuild-android-arm64-0.14.44.tgz", + "integrity": "sha512-qqaqqyxHXjZ/0ddKU3I3Nb7lAvVM69ELMhb8+91FyomAUmQPlHtxe+TTiWxXGHE72XEzcgTEGq4VauqLNkN22g==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-darwin-64": { + "version": "0.14.44", + "resolved": "https://registry.npmjs.org/esbuild-darwin-64/-/esbuild-darwin-64-0.14.44.tgz", + "integrity": "sha512-RBmtGKGY06+AW6IOJ1LE/dEeF7HH34C1/Ces9FSitU4bIbIpL4KEuQpTFoxwb4ry5s2hyw7vbPhhtyOd18FH9g==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-darwin-arm64": { + "version": "0.14.44", + "resolved": "https://registry.npmjs.org/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.14.44.tgz", + "integrity": "sha512-Bmhx5Cfo4Hdb7WyyyDupTB8HPmnFZ8baLfPlzLdYvF6OzsIbV+CY+m/AWf0OQvY40BlkzCLJ/7Lfwbb71Tngmg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-freebsd-64": { + "version": "0.14.44", + "resolved": "https://registry.npmjs.org/esbuild-freebsd-64/-/esbuild-freebsd-64-0.14.44.tgz", + "integrity": "sha512-O4HpWa5ZgxbNPQTF7URicLzYa+TidGlmGT/RAC3GjbGEQQYkd0R1Slyh69Yrmb2qmcOcPAgWHbNo1UhK4WmZ4w==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-freebsd-arm64": { + "version": "0.14.44", + "resolved": "https://registry.npmjs.org/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.14.44.tgz", + "integrity": "sha512-f0/jkAKccnDY7mg1F9l/AMzEm+VXWXK6c3IrOEmd13jyKfpTZKTIlt+yI04THPDCDZTzXHTRUBLozqp+m8Mg5Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-32": { + "version": "0.14.44", + "resolved": "https://registry.npmjs.org/esbuild-linux-32/-/esbuild-linux-32-0.14.44.tgz", + "integrity": "sha512-WSIhzLldMR7YUoEL7Ix319tC+NFmW9Pu7NgFWxUfOXeWsT0Wg484hm6bNgs7+oY2pGzg715y/Wrqi1uNOMmZJw==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-64": { + "version": "0.14.44", + "resolved": "https://registry.npmjs.org/esbuild-linux-64/-/esbuild-linux-64-0.14.44.tgz", + "integrity": "sha512-zgscTrCMcRZRIsVugqBTP/B5lPLNchBlWjQ8sQq2Epnv+UDtYKgXEq1ctWAmibZNy2E9QRCItKMeIEqeTUT5kA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-arm": { + "version": "0.14.44", + "resolved": "https://registry.npmjs.org/esbuild-linux-arm/-/esbuild-linux-arm-0.14.44.tgz", + "integrity": "sha512-laPBPwGfsbBxGw6F6jnqic2CPXLyC1bPrmnSOeJ9oEnx1rcKkizd4HWCRUc0xv+l4z/USRfx/sEfYlWSLeqoJQ==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-arm64": { + "version": "0.14.44", + "resolved": "https://registry.npmjs.org/esbuild-linux-arm64/-/esbuild-linux-arm64-0.14.44.tgz", + "integrity": "sha512-H0H/2/wgiScTwBve/JR8/o+Zhabx5KPk8T2mkYZFKQGl1hpUgC+AOmRyqy/Js3p66Wim4F4Akv3I3sJA1sKg0w==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-mips64le": { + "version": "0.14.44", + "resolved": "https://registry.npmjs.org/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.14.44.tgz", + "integrity": "sha512-ri3Okw0aleYy7o5n9zlIq+FCtq3tcMlctN6X1H1ucILjBJuH8pan2trJPKWeb8ppntFvE28I9eEXhwkWh6wYKg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-ppc64le": { + "version": "0.14.44", + "resolved": "https://registry.npmjs.org/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.14.44.tgz", + "integrity": "sha512-96TqL/MvFRuIVXz+GtCIXzRQ43ZwEk4XTn0RWUNJduXXMDQ/V1iOV28U6x6Oe3NesK4xkoKSaK2+F3VHcU8ZrA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-riscv64": { + "version": "0.14.44", + "resolved": "https://registry.npmjs.org/esbuild-linux-riscv64/-/esbuild-linux-riscv64-0.14.44.tgz", + "integrity": "sha512-rrK9qEp2M8dhilsPn4T9gxUsAumkITc1kqYbpyNMr9EWo+J5ZBj04n3GYldULrcCw4ZCHAJ+qPjqr8b6kG2inA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-s390x": { + "version": "0.14.44", + "resolved": "https://registry.npmjs.org/esbuild-linux-s390x/-/esbuild-linux-s390x-0.14.44.tgz", + "integrity": "sha512-2YmTm9BrW5aUwBSe8wIEARd9EcnOQmkHp4+IVaO09Ez/C5T866x+ABzhG0bwx0b+QRo9q97CRMaQx2Ngb6/hfw==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-netbsd-64": { + "version": "0.14.44", + "resolved": "https://registry.npmjs.org/esbuild-netbsd-64/-/esbuild-netbsd-64-0.14.44.tgz", + "integrity": "sha512-zypdzPmZTCqYS30WHxbcvtC0E6e/ECvl4WueUdbdWhs2dfWJt5RtCBME664EpTznixR3lSN1MQ2NhwQF8MQryw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-openbsd-64": { + "version": "0.14.44", + "resolved": "https://registry.npmjs.org/esbuild-openbsd-64/-/esbuild-openbsd-64-0.14.44.tgz", + "integrity": "sha512-8J43ab9ByYl7KteC03HGQjr2HY1ge7sN04lFnwMFWYk2NCn8IuaeeThvLeNjzOYhyT3I6K8puJP0uVXUu+D1xw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-sunos-64": { + "version": "0.14.44", + "resolved": "https://registry.npmjs.org/esbuild-sunos-64/-/esbuild-sunos-64-0.14.44.tgz", + "integrity": "sha512-OH1/09CGUJwffA+HNM6mqPkSIyHVC3ZnURU/4CCIx7IqWUBn1Sh1HRLQC8/TWNgcs0/1u7ygnc2pgf/AHZJ/Ow==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-windows-32": { + "version": "0.14.44", + "resolved": "https://registry.npmjs.org/esbuild-windows-32/-/esbuild-windows-32-0.14.44.tgz", + "integrity": "sha512-mCAOL9/rRqwfOfxTu2sjq/eAIs7eAXGiU6sPBnowggI7QS953Iq6o3/uDu010LwfN7zr18c/lEj6/PTwwTB3AA==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-windows-64": { + "version": "0.14.44", + "resolved": "https://registry.npmjs.org/esbuild-windows-64/-/esbuild-windows-64-0.14.44.tgz", + "integrity": "sha512-AG6BH3+YG0s2Q/IfB1cm68FdyFnoE1P+GFbmgFO3tA4UIP8+BKsmKGGZ5I3+ZjcnzOwvT74bQRVrfnQow2KO5Q==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-windows-arm64": { + "version": "0.14.44", + "resolved": "https://registry.npmjs.org/esbuild-windows-arm64/-/esbuild-windows-arm64-0.14.44.tgz", + "integrity": "sha512-ygYPfYE5By4Sd6szsNr10B0RtWVNOSGmZABSaj4YQBLqh9b9i45VAjVWa8tyIy+UAbKF7WGwybi2wTbSVliO8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/escalade": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", + "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "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": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/eslint": { + "version": "8.17.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.17.0.tgz", + "integrity": "sha512-gq0m0BTJfci60Fz4nczYxNAlED+sMcihltndR8t9t1evnU/azx53x3t2UHXC/uRjcbvRw/XctpaNygSTcQD+Iw==", + "dev": true, + "dependencies": { + "@eslint/eslintrc": "^1.3.0", + "@humanwhocodes/config-array": "^0.9.2", + "ajv": "^6.10.0", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.3.2", + "doctrine": "^3.0.0", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^7.1.1", + "eslint-utils": "^3.0.0", + "eslint-visitor-keys": "^3.3.0", + "espree": "^9.3.2", + "esquery": "^1.4.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "functional-red-black-tree": "^1.0.1", + "glob-parent": "^6.0.1", + "globals": "^13.15.0", + "ignore": "^5.2.0", + "import-fresh": "^3.0.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "js-yaml": "^4.1.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.1", + "regexpp": "^3.2.0", + "strip-ansi": "^6.0.1", + "strip-json-comments": "^3.1.0", + "text-table": "^0.2.0", + "v8-compile-cache": "^2.0.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-plugin-react": { + "version": "7.30.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.30.0.tgz", + "integrity": "sha512-RgwH7hjW48BleKsYyHK5vUAvxtE9SMPDKmcPRQgtRCYaZA0XQPt5FSkrU3nhz5ifzMZcA8opwmRJ2cmOO8tr5A==", + "dev": true, + "dependencies": { + "array-includes": "^3.1.5", + "array.prototype.flatmap": "^1.3.0", + "doctrine": "^2.1.0", + "estraverse": "^5.3.0", + "jsx-ast-utils": "^2.4.1 || ^3.0.0", + "minimatch": "^3.1.2", + "object.entries": "^1.1.5", + "object.fromentries": "^2.0.5", + "object.hasown": "^1.1.1", + "object.values": "^1.1.5", + "prop-types": "^15.8.1", + "resolve": "^2.0.0-next.3", + "semver": "^6.3.0", + "string.prototype.matchall": "^4.0.7" + }, + "engines": { + "node": ">=4" + }, + "peerDependencies": { + "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8" + } + }, + "node_modules/eslint-plugin-react/node_modules/doctrine": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", + "dev": true, + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/eslint-plugin-react/node_modules/resolve": { + "version": "2.0.0-next.3", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.3.tgz", + "integrity": "sha512-W8LucSynKUIDu9ylraa7ueVZ7hc0uAgJBxVsQSKOXOyle8a93qXhcz+XAXZ8bIq2d6i4Ehddn6Evt+0/UwKk6Q==", + "dev": true, + "dependencies": { + "is-core-module": "^2.2.0", + "path-parse": "^1.0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/eslint-plugin-react/node_modules/semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "dev": true, + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/eslint-scope/node_modules/estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/eslint-utils": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-3.0.0.tgz", + "integrity": "sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA==", + "dev": true, + "dependencies": { + "eslint-visitor-keys": "^2.0.0" + }, + "engines": { + "node": "^10.0.0 || ^12.0.0 || >= 14.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/mysticatea" + }, + "peerDependencies": { + "eslint": ">=5" + } + }, + "node_modules/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz", + "integrity": "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.3.0.tgz", + "integrity": "sha512-mQ+suqKJVyeuwGYHAdjMFqjCyfl8+Ldnxuyp3ldiMBFKkvytrXUZWaiPCEav8qDHKty44bD+qV1IP4T+w+xXRA==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/eslint/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/eslint/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/eslint/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/eslint/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/eslint/node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint/node_modules/eslint-scope": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.1.1.tgz", + "integrity": "sha512-QKQM/UXpIiHcLqJ5AOyIW7XZmzjkzQXYE54n1++wb0u9V/abW3l9uQnxX8Z5Xd18xyKIMTUAyQ0k1e8pz6LUrw==", + "dev": true, + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/eslint/node_modules/globals": { + "version": "13.15.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.15.0.tgz", + "integrity": "sha512-bpzcOlgDhMG070Av0Vy5Owklpv1I6+j96GhUI7Rh7IzDCKLzboflLrrfqMu8NquDbiR4EOQk7XzJwqVJxicxog==", + "dev": true, + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint/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/eslint/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/espree": { + "version": "9.3.2", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.3.2.tgz", + "integrity": "sha512-D211tC7ZwouTIuY5x9XnS0E9sWNChB7IYKX/Xp5eQj3nFXhqmiUDB9q27y76oFl8jTg3pXcQx/bpxMfs3CIZbA==", + "dev": true, + "dependencies": { + "acorn": "^8.7.1", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/esquery": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.4.0.tgz", + "integrity": "sha512-cCDispWt5vHHtwMY2YrAQ4ibFkAL8RbH5YGBnZBc90MolvvfkkQcJro/aZiAQUlQ3qgrYS6D6v8Gc5G5CQsc9w==", + "dev": true, + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "engines": { + "node": ">=4.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/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" + }, + "node_modules/fast-glob": { + "version": "3.2.11", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.11.tgz", + "integrity": "sha512-xrO3+1bxSo3ZVHAnqzyuewYT6aMFHRAd4Kcs92MAonjwQZLsK9d0SF1IyQ3k5PoirxTW0Oe/RqFgMQ6TcNE5Ew==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/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/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true + }, + "node_modules/fastq": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.13.0.tgz", + "integrity": "sha512-YpkpUnK8od0o1hmeSc7UUs/eB/vIPWJYjKck2QKIzAf71Vm1AAQ3EbuZB3g2JIy+pg+ERD0vqI79KyZiB2e2Nw==", + "dev": true, + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/file-entry-cache": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "dev": true, + "dependencies": { + "flat-cache": "^3.0.4" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "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/find-root": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz", + "integrity": "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==" + }, + "node_modules/find-up": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", + "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", + "dependencies": { + "locate-path": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/flat-cache": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.0.4.tgz", + "integrity": "sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg==", + "dev": true, + "dependencies": { + "flatted": "^3.1.0", + "rimraf": "^3.0.2" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/flatted": { + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.5.tgz", + "integrity": "sha512-WIWGi2L3DyTUvUrwRKgGi9TwxQMUEqPOPQBVi71R96jZXJdFskXEmf54BoZaS1kknGODoIGASGEzBUYdyMCBJg==", + "dev": true + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==" + }, + "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==" + }, + "node_modules/function.prototype.name": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.5.tgz", + "integrity": "sha512-uN7m/BzVKQnCUF/iW8jYea67v++2u7m5UgENbHRtdDVclOUP+FMPlCNdmk0h/ysGyo2tavMJEDqJAkJdRa1vMA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3", + "es-abstract": "^1.19.0", + "functions-have-names": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/functional-red-black-tree": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz", + "integrity": "sha512-dsKNQNdj6xA3T+QlADDA7mOSlX0qiMINjn0cgr+eGHGsbSHzTabcIogz2+p/iqP1Xs6EP/sS2SbqH+brGTbq0g==", + "dev": true + }, + "node_modules/functions-have-names": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", + "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.2.tgz", + "integrity": "sha512-Jfm3OyCxHh9DJyc28qGk+JmfkpO41A4XkneDSujN9MDXrm4oDKdHvndhZ2dN94+ERNfkYJWDclW6k2L/ZGHjXA==", + "dev": true, + "dependencies": { + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-symbol-description": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.0.tgz", + "integrity": "sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/globby": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "dev": true, + "dependencies": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/goober": { + "version": "2.1.13", + "resolved": "https://registry.npmjs.org/goober/-/goober-2.1.13.tgz", + "integrity": "sha512-jFj3BQeleOoy7t93E9rZ2de+ScC4lQICLwiAQmKMg9F6roKGaLSHoCDYKkWlSafg138jejvq/mTdvmnwDQgqoQ==", + "peerDependencies": { + "csstype": "^3.0.10" + } + }, + "node_modules/has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "dependencies": { + "function-bind": "^1.1.1" + }, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/has-bigints": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz", + "integrity": "sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "engines": { + "node": ">=4" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.0.tgz", + "integrity": "sha512-62DVLZGoiEBDHQyqG4w9xCuZ7eJEwNmJRWw2VY84Oedb7WFcA27fiEVe8oUQx9hAUJ4ekurquucTGwsyO1XGdQ==", + "dev": true, + "dependencies": { + "get-intrinsic": "^1.1.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.0.tgz", + "integrity": "sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==", + "dev": true, + "dependencies": { + "has-symbols": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/highlight.js": { + "version": "11.5.1", + "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-11.5.1.tgz", + "integrity": "sha512-LKzHqnxr4CrD2YsNoIf/o5nJ09j4yi/GcH5BnYz9UnVpZdS4ucMgvP61TDty5xJcFGRjnH4DpujkS9bHT3hq0Q==", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/hoist-non-react-statics": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", + "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", + "dependencies": { + "react-is": "^16.7.0" + } + }, + "node_modules/hoist-non-react-statics/node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" + }, + "node_modules/ignore": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.0.tgz", + "integrity": "sha512-CmxgYGiEPCLhfLnpPp1MoRmifwEIOgjcHXxOBjv7mY96c+eWScsOP9c112ZyLdWHi0FxHjI+4uVhKYp/gcdRmQ==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/immutable": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.1.0.tgz", + "integrity": "sha512-oNkuqVTA8jqG1Q6c+UglTOD1xhC1BtjKI7XkCXRkZHrN5m18/XsnUp8Q89GkQO/z+0WjonSvl0FLhDYftp46nQ==", + "dev": true + }, + "node_modules/import-fresh": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", + "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "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==" + }, + "node_modules/internal-slot": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.3.tgz", + "integrity": "sha512-O0DB1JC/sPyZl7cIo78n5dR7eUSwwpYPiXRhTzNxZVAMUuB8vlnRFyLxdrVToks6XPLVnFfbzaVd5WLjhgg+vA==", + "dev": true, + "dependencies": { + "get-intrinsic": "^1.1.0", + "has": "^1.0.3", + "side-channel": "^1.0.4" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==" + }, + "node_modules/is-bigint": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.4.tgz", + "integrity": "sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==", + "dev": true, + "dependencies": { + "has-bigints": "^1.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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-boolean-object": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz", + "integrity": "sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-callable": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.4.tgz", + "integrity": "sha512-nsuwtxZfMX67Oryl9LCQ+upnC0Z0BgpwntpS89m1H/TLF0zNfzfLMV/9Wa/6MZsj0acpEjAO0KF1xT6ZdLl95w==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-core-module": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.9.0.tgz", + "integrity": "sha512-+5FPy5PnwmO3lvfMb0AsoPaBG+5KHUI0wYFXOtYPnVVVspTFUuMZNfNaNVRt3FZadstu2c8x23vykRW/NBoU6A==", + "dependencies": { + "has": "^1.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-date-object": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz", + "integrity": "sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==", + "dev": true, + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-docker": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", + "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", + "dev": true, + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "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-negative-zero": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.2.tgz", + "integrity": "sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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-number-object": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.7.tgz", + "integrity": "sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==", + "dev": true, + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-regex": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", + "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-shared-array-buffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.2.tgz", + "integrity": "sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-string": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz", + "integrity": "sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==", + "dev": true, + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-symbol": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.4.tgz", + "integrity": "sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==", + "dev": true, + "dependencies": { + "has-symbols": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakref": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.2.tgz", + "integrity": "sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-wsl": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", + "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", + "dev": true, + "dependencies": { + "is-docker": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true + }, + "node_modules/jest-diff": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-26.6.2.tgz", + "integrity": "sha512-6m+9Z3Gv9wN0WFVasqjCL/06+EFCMTqDEUl/b87HYK2rAPTyfz4ZIuSlPhY51PIQRWx5TaxeF1qmXKe9gfN3sA==", + "dev": true, + "dependencies": { + "chalk": "^4.0.0", + "diff-sequences": "^26.6.2", + "jest-get-type": "^26.3.0", + "pretty-format": "^26.6.2" + }, + "engines": { + "node": ">= 10.14.2" + } + }, + "node_modules/jest-diff/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-diff/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jest-diff/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/jest-diff/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/jest-diff/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-diff/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/jest-get-type": { + "version": "26.3.0", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-26.3.0.tgz", + "integrity": "sha512-TpfaviN1R2pQWkIihlfEanwOXK0zcxrKEE4MlU6Tn7keoXdN6/3gK/xl0yEh8DOunn5pOVGKf8hB4R9gVh04ig==", + "dev": true, + "engines": { + "node": ">= 10.14.2" + } + }, + "node_modules/js-sha3": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/js-sha3/-/js-sha3-0.8.0.tgz", + "integrity": "sha512-gF1cRrHhIzNfToc802P800N8PpXS+evLLXfsVpowqmAFR9uwbi89WvXg2QspOmXL8QL86J4T1EpFu+yUkwJY3Q==" + }, + "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==" + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", + "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", + "dev": true, + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true + }, + "node_modules/json5": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.1.tgz", + "integrity": "sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA==", + "dev": true, + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jsx-ast-utils": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.0.tgz", + "integrity": "sha512-XzO9luP6L0xkxwhIJMTJQpZo/eeN60K08jHdexfD569AGxeNug6UketeHXEhROoM8aR7EcUoOQmIhcJQjcuq8Q==", + "dev": true, + "dependencies": { + "array-includes": "^3.1.4", + "object.assign": "^4.1.2" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==" + }, + "node_modules/locate-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", + "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", + "dependencies": { + "p-locate": "^3.0.0", + "path-exists": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + }, + "node_modules/lodash-es": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz", + "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==" + }, + "node_modules/lodash.debounce": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", + "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==" + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/match-sorter": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/match-sorter/-/match-sorter-6.3.1.tgz", + "integrity": "sha512-mxybbo3pPNuA+ZuCUhm5bwNkXrJTbsk5VWbR5wiwz/GC6LIiegBGn2w3O08UG/jdbYLinw51fSQ5xNU1U3MgBw==", + "dependencies": { + "@babel/runtime": "^7.12.5", + "remove-accents": "0.4.2" + } + }, + "node_modules/memoize-one": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-5.2.1.tgz", + "integrity": "sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==" + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", + "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "dev": true, + "dependencies": { + "braces": "^3.0.2", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/microseconds": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/microseconds/-/microseconds-0.2.0.tgz", + "integrity": "sha512-n7DHHMjR1avBbSpsTBj6fmMGh2AGrifVV4e+WYc3Q9lO+xnSZ3NyhcBND3vzzatt05LFhoKFRxrIyklmLlUtyA==" + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/moment": { + "version": "2.29.4", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.4.tgz", + "integrity": "sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w==", + "engines": { + "node": "*" + } + }, + "node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "node_modules/nano-time": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/nano-time/-/nano-time-1.0.0.tgz", + "integrity": "sha512-flnngywOoQ0lLQOTRNexn2gGSNuM9bKj9RZAWSzhQ+UJYaAFG9bac4DW9VHjUAzrOaIcajHybCTHe/bkvozQqA==", + "dependencies": { + "big-integer": "^1.6.16" + } + }, + "node_modules/nanoclone": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/nanoclone/-/nanoclone-0.2.1.tgz", + "integrity": "sha512-wynEP02LmIbLpcYw8uBKpcfF6dmg2vcpKqxeH5UcoKEYdExslsdUA4ugFauuaeYdTB76ez6gJW8XAZ6CgkXYxA==" + }, + "node_modules/nanoid": { + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.4.tgz", + "integrity": "sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw==", + "dev": true, + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true + }, + "node_modules/node-releases": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.5.tgz", + "integrity": "sha512-U9h1NLROZTq9uE1SNffn6WuPDg8icmi3ns4rEl/oTfIle4iLjTliCzgTsbaIFMq/Xn078/lfY/BL0GWZ+psK4Q==", + "dev": true + }, + "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/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "engines": { + "node": ">= 6" + } + }, + "node_modules/object-inspect": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.2.tgz", + "integrity": "sha512-z+cPxW0QGUp0mcqcsgQyLVRDoXFQbXOwBaqyF7VIgI4TWNQsDHrBpUQslRmIfAoYWdYzs6UlKJtB2XJpTaNSpQ==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.assign": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.2.tgz", + "integrity": "sha512-ixT2L5THXsApyiUPYKmW+2EHpXXe5Ii3M+f4e+aJFAHao5amFRW6J0OO6c/LU8Be47utCx2GL89hxGB6XSmKuQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.0", + "define-properties": "^1.1.3", + "has-symbols": "^1.0.1", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.entries": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.5.tgz", + "integrity": "sha512-TyxmjUoZggd4OrrU1W66FMDG6CuqJxsFvymeyXI51+vQLN67zYfZseptRge703kKQdo4uccgAKebXFcRCzk4+g==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3", + "es-abstract": "^1.19.1" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.fromentries": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.5.tgz", + "integrity": "sha512-CAyG5mWQRRiBU57Re4FKoTBjXfDoNwdFVH2Y1tS9PqCsfUTymAohOkEMSG3aRNKmv4lV3O7p1et7c187q6bynw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3", + "es-abstract": "^1.19.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.hasown": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object.hasown/-/object.hasown-1.1.1.tgz", + "integrity": "sha512-LYLe4tivNQzq4JdaWW6WO3HMZZJWzkkH8fnI6EebWl0VZth2wL2Lovm74ep2/gZzlaTdV62JZHEqHQ2yVn8Q/A==", + "dev": true, + "dependencies": { + "define-properties": "^1.1.4", + "es-abstract": "^1.19.5" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.values": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.1.5.tgz", + "integrity": "sha512-QUZRW0ilQ3PnPpbNtgdNV1PDbEqLIiSFB3l+EnGtBQ/8SUTLj1PZwtQHABZtLgwpJZTSZhuGLOGk57Drx2IvYg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3", + "es-abstract": "^1.19.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/oblivious-set": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/oblivious-set/-/oblivious-set-1.0.0.tgz", + "integrity": "sha512-z+pI07qxo4c2CulUHCDf9lcqDlMSo72N/4rLUpRXf6fu+q8vjt8y0xS+Tlf8NTJDdTXHbdeO1n3MlbctwEoXZw==" + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/open": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/open/-/open-8.4.0.tgz", + "integrity": "sha512-XgFPPM+B28FtCCgSb9I+s9szOC1vZRSwgWsRUA5ylIxRTgKozqjOCrVOqGsYABPYK5qnfqClxZTFBa8PKt2v6Q==", + "dev": true, + "dependencies": { + "define-lazy-prop": "^2.0.0", + "is-docker": "^2.1.1", + "is-wsl": "^2.2.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/optionator": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz", + "integrity": "sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==", + "dev": true, + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.3" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", + "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", + "dependencies": { + "p-limit": "^2.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "engines": { + "node": ">=6" + } + }, + "node_modules/packet-reader": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/packet-reader/-/packet-reader-1.0.0.tgz", + "integrity": "sha512-HAKu/fG3HpHFO0AA8WE8q2g+gBJaZ9MG7fcKk+IJPLTGAD6Psw4443l+9DGRbOIh3/aXr7Phy0TjilYivJo5XQ==", + "dev": true + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/path-exists": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", + "integrity": "sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==", + "engines": { + "node": ">=4" + } + }, + "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": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "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==" + }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "engines": { + "node": ">=8" + } + }, + "node_modules/pg": { + "version": "8.11.3", + "resolved": "https://registry.npmjs.org/pg/-/pg-8.11.3.tgz", + "integrity": "sha512-+9iuvG8QfaaUrrph+kpF24cXkH1YOOUeArRNYIxq1viYHZagBxrTno7cecY1Fa44tJeZvaoG+Djpkc3JwehN5g==", + "dev": true, + "dependencies": { + "buffer-writer": "2.0.0", + "packet-reader": "1.0.0", + "pg-connection-string": "^2.6.2", + "pg-pool": "^3.6.1", + "pg-protocol": "^1.6.0", + "pg-types": "^2.1.0", + "pgpass": "1.x" + }, + "engines": { + "node": ">= 8.0.0" + }, + "optionalDependencies": { + "pg-cloudflare": "^1.1.1" + }, + "peerDependencies": { + "pg-native": ">=3.0.1" + }, + "peerDependenciesMeta": { + "pg-native": { + "optional": true + } + } + }, + "node_modules/pg-cloudflare": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.1.1.tgz", + "integrity": "sha512-xWPagP/4B6BgFO+EKz3JONXv3YDgvkbVrGw2mTo3D6tVDQRh1e7cqVGvyR3BE+eQgAvx1XhW/iEASj4/jCWl3Q==", + "dev": true, + "optional": true + }, + "node_modules/pg-connection-string": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.6.2.tgz", + "integrity": "sha512-ch6OwaeaPYcova4kKZ15sbJ2hKb/VP48ZD2gE7i1J+L4MspCtBMAx8nMgz7bksc7IojCIIWuEhHibSMFH8m8oA==", + "dev": true + }, + "node_modules/pg-int8": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz", + "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==", + "dev": true, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/pg-pool": { + "version": "3.6.1", + "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.6.1.tgz", + "integrity": "sha512-jizsIzhkIitxCGfPRzJn1ZdcosIt3pz9Sh3V01fm1vZnbnCMgmGl5wvGGdNN2EL9Rmb0EcFoCkixH4Pu+sP9Og==", + "dev": true, + "peerDependencies": { + "pg": ">=8.0" + } + }, + "node_modules/pg-protocol": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.6.0.tgz", + "integrity": "sha512-M+PDm637OY5WM307051+bsDia5Xej6d9IR4GwJse1qA1DIhiKlksvrneZOYQq42OM+spubpcNYEo2FcKQrDk+Q==", + "dev": true + }, + "node_modules/pg-types": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz", + "integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==", + "dev": true, + "dependencies": { + "pg-int8": "1.0.1", + "postgres-array": "~2.0.0", + "postgres-bytea": "~1.0.0", + "postgres-date": "~1.0.4", + "postgres-interval": "^1.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/pgpass": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz", + "integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==", + "dev": true, + "dependencies": { + "split2": "^4.1.0" + } + }, + "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 + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/playwright": { + "version": "1.41.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.41.1.tgz", + "integrity": "sha512-gdZAWG97oUnbBdRL3GuBvX3nDDmUOuqzV/D24dytqlKt+eI5KbwusluZRGljx1YoJKZ2NRPaeWiFTeGZO7SosQ==", + "dev": true, + "dependencies": { + "playwright-core": "1.41.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=16" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.41.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.41.1.tgz", + "integrity": "sha512-/KPO5DzXSMlxSX77wy+HihKGOunh3hqndhqeo/nMxfigiKzogn8kfL0ZBDu0L1RKgan5XHCPmn6zXd2NUJgjhg==", + "dev": true, + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/postcss": { + "version": "8.4.14", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.14.tgz", + "integrity": "sha512-E398TUmfAYFPBSdzgeieK2Y1+1cpdxJx8yXbK/m57nRhKSmk1GB2tO4lbLBtlkfPQTDKfe4Xqv1ASWPpayPEig==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + } + ], + "dependencies": { + "nanoid": "^3.3.4", + "picocolors": "^1.0.0", + "source-map-js": "^1.0.2" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postgres-array": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz", + "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/postgres-bytea": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.0.tgz", + "integrity": "sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-date": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz", + "integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-interval": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz", + "integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==", + "dev": true, + "dependencies": { + "xtend": "^4.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prettier": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.6.2.tgz", + "integrity": "sha512-PkUpF+qoXTqhOeWL9fu7As8LXsIUZ1WYaJiY/a7McAQzxjk82OF0tibkFXVCDImZtWxbvojFjerkiLb0/q8mew==", + "dev": true, + "bin": { + "prettier": "bin-prettier.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/pretty-format": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-26.6.2.tgz", + "integrity": "sha512-7AeGuCYNGmycyQbCqd/3PWH4eOoX/OiCa0uphp57NVTeAGdJGaAliecxwBDHYQCIvrW7aDBZCYeNTP/WX69mkg==", + "dev": true, + "dependencies": { + "@jest/types": "^26.6.2", + "ansi-regex": "^5.0.0", + "ansi-styles": "^4.0.0", + "react-is": "^17.0.1" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/pretty-format/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/pretty-format/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", + "dev": true, + "engines": { + "node": ">= 0.6.0" + } + }, + "node_modules/prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, + "node_modules/prop-types/node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" + }, + "node_modules/property-expr": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/property-expr/-/property-expr-2.0.5.tgz", + "integrity": "sha512-IJUkICM5dP5znhCckHSv30Q4b5/JA5enCtkRHYaOVOAocnH/1BQEYTC5NMfT3AVl/iXKdr3aqQbQn9DxyWknwA==" + }, + "node_modules/punycode": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", + "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "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/react": { + "version": "18.1.0", + "resolved": "https://registry.npmjs.org/react/-/react-18.1.0.tgz", + "integrity": "sha512-4oL8ivCz5ZEPyclFQXaNksK3adutVS8l2xzZU0cqEFrE9Sb7fC0EFK5uEk74wIreL1DERyjvsU915j1pcT2uEQ==", + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "18.1.0", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.1.0.tgz", + "integrity": "sha512-fU1Txz7Budmvamp7bshe4Zi32d0ll7ect+ccxNu9FlObT605GOEB8BfO4tmRJ39R5Zj831VCpvQ05QPBW5yb+w==", + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.22.0" + }, + "peerDependencies": { + "react": "^18.1.0" + } + }, + "node_modules/react-hook-form": { + "version": "7.48.2", + "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.48.2.tgz", + "integrity": "sha512-H0T2InFQb1hX7qKtDIZmvpU1Xfn/bdahWBN1fH19gSe4bBEqTfmlr7H3XWTaVtiK4/tpPaI1F3355GPMZYge+A==", + "engines": { + "node": ">=12.22.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/react-hook-form" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17 || ^18" + } + }, + "node_modules/react-hot-toast": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/react-hot-toast/-/react-hot-toast-2.4.1.tgz", + "integrity": "sha512-j8z+cQbWIM5LY37pR6uZR6D4LfseplqnuAO4co4u8917hBUvXlEqyP1ZzqVLcqoyUesZZv/ImreoCeHVDpE5pQ==", + "dependencies": { + "goober": "^2.1.10" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "react": ">=16", + "react-dom": ">=16" + } + }, + "node_modules/react-is": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", + "dev": true + }, + "node_modules/react-query": { + "version": "3.39.1", + "resolved": "https://registry.npmjs.org/react-query/-/react-query-3.39.1.tgz", + "integrity": "sha512-qYKT1bavdDiQZbngWZyPotlBVzcBjDYEJg5RQLBa++5Ix5jjfbEYJmHSZRZD+USVHUSvl/ey9Hu+QfF1QAK80A==", + "dependencies": { + "@babel/runtime": "^7.5.5", + "broadcast-channel": "^3.4.1", + "match-sorter": "^6.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + }, + "react-native": { + "optional": true + } + } + }, + "node_modules/react-refresh": { + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.13.0.tgz", + "integrity": "sha512-XP8A9BT0CpRBD+NYLLeIhld/RqG9+gktUjW1FkE+Vm7OCinbG1SshcK5tb9ls4kzvjZr9mOQc7HYgBngEyPAXg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-router": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.4.0.tgz", + "integrity": "sha512-B+5bEXFlgR1XUdHYR6P94g299SjrfCBMmEDJNcFbpAyRH1j1748yt9NdDhW3++nw1lk3zQJ6aOO66zUx3KlTZg==", + "dependencies": { + "@remix-run/router": "1.0.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "react": ">=16.8" + } + }, + "node_modules/react-router-dom": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.4.0.tgz", + "integrity": "sha512-4Aw1xmXKeleYYQ3x0Lcl2undHR6yMjXZjd9DKZd53SGOYqirrUThyUb0wwAX5VZAyvSuzjNJmZlJ3rR9+/vzqg==", + "dependencies": { + "react-router": "6.4.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, + "node_modules/react-select": { + "version": "5.7.7", + "resolved": "https://registry.npmjs.org/react-select/-/react-select-5.7.7.tgz", + "integrity": "sha512-HhashZZJDRlfF/AKj0a0Lnfs3sRdw/46VJIRd8IbB9/Ovr74+ZIwkAdSBjSPXsFMG+u72c5xShqwLSKIJllzqw==", + "dependencies": { + "@babel/runtime": "^7.12.0", + "@emotion/cache": "^11.4.0", + "@emotion/react": "^11.8.1", + "@floating-ui/dom": "^1.0.1", + "@types/react-transition-group": "^4.4.0", + "memoize-one": "^6.0.0", + "prop-types": "^15.6.0", + "react-transition-group": "^4.3.0", + "use-isomorphic-layout-effect": "^1.1.2" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/react-select/node_modules/memoize-one": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-6.0.0.tgz", + "integrity": "sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==" + }, + "node_modules/react-tooltip": { + "version": "5.14.0", + "resolved": "https://registry.npmjs.org/react-tooltip/-/react-tooltip-5.14.0.tgz", + "integrity": "sha512-fvHgZKSdDC/Zv7G7IL6kPJi99Fqty6zkQsYQZGSkhMmmDQhVnsBLAmY8s14Nh8bshgPmR5EvbjUDZg0yMsp1hg==", + "dependencies": { + "@floating-ui/dom": "^1.0.0", + "classnames": "^2.3.0" + }, + "peerDependencies": { + "react": ">=16.14.0", + "react-dom": ">=16.14.0" + } + }, + "node_modules/react-transition-group": { + "version": "4.4.5", + "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz", + "integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==", + "dependencies": { + "@babel/runtime": "^7.5.5", + "dom-helpers": "^5.0.1", + "loose-envify": "^1.4.0", + "prop-types": "^15.6.2" + }, + "peerDependencies": { + "react": ">=16.6.0", + "react-dom": ">=16.6.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/regenerator-runtime": { + "version": "0.13.11", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", + "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==" + }, + "node_modules/regexp.prototype.flags": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.4.3.tgz", + "integrity": "sha512-fjggEOO3slI6Wvgjwflkc4NFRCTZAu5CnNfBd5qOMYhWdn67nJBBu34/TkD++eeFmd8C9r9jfXJ27+nSiRkSUA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3", + "functions-have-names": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/regexpp": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.2.0.tgz", + "integrity": "sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/mysticatea" + } + }, + "node_modules/remove-accents": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/remove-accents/-/remove-accents-0.4.2.tgz", + "integrity": "sha512-7pXIJqJOq5tFgG1A2Zxti3Ht8jJF337m4sowbuHsW30ZnkQFnDzy9qBNhgzX8ZLW4+UBcXiiR7SwR6pokHsxiA==" + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-main-filename": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", + "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==" + }, + "node_modules/resolve": { + "version": "1.22.0", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.0.tgz", + "integrity": "sha512-Hhtrw0nLeSrFQ7phPp4OOcVjLPIeMnRlr5mcnVuMe7M/7eBn98A3hmFRLoFo3DLZkivSYwhRUJTyPyWAk56WLw==", + "dependencies": { + "is-core-module": "^2.8.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "engines": { + "node": ">=4" + } + }, + "node_modules/reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "dev": true, + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rifm": { + "version": "0.12.1", + "resolved": "https://registry.npmjs.org/rifm/-/rifm-0.12.1.tgz", + "integrity": "sha512-OGA1Bitg/dSJtI/c4dh90svzaUPt228kzFsUkJbtA2c964IqEAwWXeL9ZJi86xWv3j5SMqRvGULl7bA6cK0Bvg==", + "peerDependencies": { + "react": ">=16.8" + } + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rollup": { + "version": "2.75.6", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.75.6.tgz", + "integrity": "sha512-OEf0TgpC9vU6WGROJIk1JA3LR5vk/yvqlzxqdrE2CzzXnqKXNzbAwlWUXis8RS3ZPe7LAq+YUxsRa0l3r27MLA==", + "dev": true, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=10.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/rollup-plugin-analyzer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/rollup-plugin-analyzer/-/rollup-plugin-analyzer-4.0.0.tgz", + "integrity": "sha512-LL9GEt3bkXp6Wa19SNR5MWcvHNMvuTFYg+eYBZN2OIFhSWN+pEJUQXEKu5BsOeABob3x9PDaLKW7w5iOJnsESQ==", + "dev": true, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/rollup-plugin-visualizer": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/rollup-plugin-visualizer/-/rollup-plugin-visualizer-5.6.0.tgz", + "integrity": "sha512-CKcc8GTUZjC+LsMytU8ocRr/cGZIfMR7+mdy4YnlyetlmIl/dM8BMnOEpD4JPIGt+ZVW7Db9ZtSsbgyeBH3uTA==", + "dev": true, + "dependencies": { + "nanoid": "^3.1.32", + "open": "^8.4.0", + "source-map": "^0.7.3", + "yargs": "^17.3.1" + }, + "bin": { + "rollup-plugin-visualizer": "dist/bin/cli.js" + }, + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "rollup": "^2.0.0" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "node_modules/sass": { + "version": "1.55.0", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.55.0.tgz", + "integrity": "sha512-Pk+PMy7OGLs9WaxZGJMn7S96dvlyVBwwtToX895WmCpAOr5YiJYEUJfiJidMuKb613z2xNWcXCHEuOvjZbqC6A==", + "dev": true, + "dependencies": { + "chokidar": ">=3.0.0 <4.0.0", + "immutable": "^4.0.0", + "source-map-js": ">=0.6.2 <2.0.0" + }, + "bin": { + "sass": "sass.js" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/scheduler": { + "version": "0.22.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.22.0.tgz", + "integrity": "sha512-6QAm1BgQI88NPYymgGQLCZgvep4FyePDWFpXVK+zNSUgHwlqpJy8VEh8Et0KxTACS4VWwMousBElAZOH9nkkoQ==", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "node_modules/semver": { + "version": "7.3.7", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.7.tgz", + "integrity": "sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g==", + "dev": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/showdown": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/showdown/-/showdown-1.9.1.tgz", + "integrity": "sha512-9cGuS382HcvExtf5AHk7Cb4pAeQQ+h0eTr33V1mu+crYWV4KvWAw6el92bDrqGEk5d46Ai/fhbEUwqJ/mTCNEA==", + "dependencies": { + "yargs": "^14.2" + }, + "bin": { + "showdown": "bin/showdown.js" + } + }, + "node_modules/showdown/node_modules/ansi-regex": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.1.tgz", + "integrity": "sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g==", + "engines": { + "node": ">=6" + } + }, + "node_modules/showdown/node_modules/cliui": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-5.0.0.tgz", + "integrity": "sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA==", + "dependencies": { + "string-width": "^3.1.0", + "strip-ansi": "^5.2.0", + "wrap-ansi": "^5.1.0" + } + }, + "node_modules/showdown/node_modules/emoji-regex": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", + "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==" + }, + "node_modules/showdown/node_modules/is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha512-VHskAKYM8RfSFXwee5t5cbN5PZeq1Wrh6qd5bkyiXIf6UQcN6w/A0eXM9r6t8d+GYOh+o6ZhiEnb88LN/Y8m2w==", + "engines": { + "node": ">=4" + } + }, + "node_modules/showdown/node_modules/string-width": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", + "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", + "dependencies": { + "emoji-regex": "^7.0.1", + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^5.1.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/showdown/node_modules/strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "dependencies": { + "ansi-regex": "^4.1.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/showdown/node_modules/wrap-ansi": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-5.1.0.tgz", + "integrity": "sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q==", + "dependencies": { + "ansi-styles": "^3.2.0", + "string-width": "^3.0.0", + "strip-ansi": "^5.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/showdown/node_modules/y18n": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", + "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==" + }, + "node_modules/showdown/node_modules/yargs": { + "version": "14.2.3", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-14.2.3.tgz", + "integrity": "sha512-ZbotRWhF+lkjijC/VhmOT9wSgyBQ7+zr13+YLkhfsSiTriYsMzkTUFP18pFhWwBeMa5gUc1MzbhrO6/VB7c9Xg==", + "dependencies": { + "cliui": "^5.0.0", + "decamelize": "^1.2.0", + "find-up": "^3.0.0", + "get-caller-file": "^2.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^3.0.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^15.0.1" + } + }, + "node_modules/showdown/node_modules/yargs-parser": { + "version": "15.0.3", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-15.0.3.tgz", + "integrity": "sha512-/MVEVjTXy/cGAjdtQf8dW3V9b97bPN7rNn8ETj6BmAQL7ibC7O1Q9SPJbGjgh3SlwoBNXMzj/ZGIj8mBgl12YA==", + "dependencies": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + } + }, + "node_modules/side-channel": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", + "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.0", + "get-intrinsic": "^1.0.2", + "object-inspect": "^1.9.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/source-map": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz", + "integrity": "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/source-map-js": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", + "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "dev": true, + "engines": { + "node": ">= 10.x" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string.prototype.matchall": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.7.tgz", + "integrity": "sha512-f48okCX7JiwVi1NXCVWcFnZgADDC/n2vePlQ/KUCNqCikLLilQvwjMO8+BHVKvgzH0JB0J9LEPgxOGT02RoETg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3", + "es-abstract": "^1.19.1", + "get-intrinsic": "^1.1.1", + "has-symbols": "^1.0.3", + "internal-slot": "^1.0.3", + "regexp.prototype.flags": "^1.4.1", + "side-channel": "^1.0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimend": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.5.tgz", + "integrity": "sha512-I7RGvmjV4pJ7O3kdf+LXFpVfdNOxtCW/2C8f6jNiW4+PQchwxkCDzlk1/7p+Wl4bqFIZeF47qAHXLuHHWKAxog==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "es-abstract": "^1.19.5" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimstart": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.5.tgz", + "integrity": "sha512-THx16TJCGlsN0o6dl2o6ncWUsdgnLRSA23rRE5pyGBw/mLr3Ej/R2LaqCtgP8VNMGZsvMWnf9ooZPyY2bHvUFg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "es-abstract": "^1.19.5" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/style-mod": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.0.0.tgz", + "integrity": "sha512-OPhtyEjyyN9x3nhPsu76f52yUGXiZcgvsrFVtvTkyGRQJ0XK+GPc6ov1z+lRpbeabka+MYEQxOYRnt5nF30aMw==" + }, + "node_modules/stylis": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.2.0.tgz", + "integrity": "sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==" + }, + "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==", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", + "dev": true + }, + "node_modules/to-fast-properties": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", + "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", + "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/toggle-selection": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/toggle-selection/-/toggle-selection-1.0.6.tgz", + "integrity": "sha512-BiZS+C1OS8g/q2RRbJmy59xpyghNBqrr6k5L/uKBGRsTfxmu3ffiRnd8mlGPUVayg8pvfi5urfnu8TU7DVOkLQ==" + }, + "node_modules/toposort": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/toposort/-/toposort-2.0.2.tgz", + "integrity": "sha512-0a5EOkAUp8D4moMi2W8ZF8jcga7BgZd91O/yabJCFY8az+XSzeGyTKs0Aoo897iV1Nj6guFq8orWDS96z91oGg==" + }, + "node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true + }, + "node_modules/tsutils": { + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz", + "integrity": "sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==", + "dev": true, + "dependencies": { + "tslib": "^1.8.1" + }, + "engines": { + "node": ">= 6" + }, + "peerDependencies": { + "typescript": ">=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta" + } + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/typescript": { + "version": "4.7.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.7.3.tgz", + "integrity": "sha512-WOkT3XYvrpXx4vMMqlD+8R8R37fZkjyLGlxavMc4iB8lrl8L0DeTcHbYgw/v0N/z9wAFsgBhcsF0ruoySS22mA==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=4.2.0" + } + }, + "node_modules/unbox-primitive": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz", + "integrity": "sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "has-bigints": "^1.0.2", + "has-symbols": "^1.0.3", + "which-boxed-primitive": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/unload": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/unload/-/unload-2.2.0.tgz", + "integrity": "sha512-B60uB5TNBLtN6/LsgAf3udH9saB5p7gqJwcFfbOEZ8BcBHnGwCf6G/TGiEqkRAxX7zAFIUtzdrXQSdL3Q/wqNA==", + "dependencies": { + "@babel/runtime": "^7.6.2", + "detect-node": "^2.0.4" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/use-isomorphic-layout-effect": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/use-isomorphic-layout-effect/-/use-isomorphic-layout-effect-1.1.2.tgz", + "integrity": "sha512-49L8yCO3iGT/ZF9QttjwLF/ZD9Iwto5LnH5LmEdk/6cFmXddqi2ulF0edxTwjj+7mqvpVVGQWvbXZdn32wRSHA==", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/use-sync-external-store": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz", + "integrity": "sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/v8-compile-cache": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz", + "integrity": "sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA==", + "dev": true + }, + "node_modules/vite": { + "version": "2.9.12", + "resolved": "https://registry.npmjs.org/vite/-/vite-2.9.12.tgz", + "integrity": "sha512-suxC36dQo9Rq1qMB2qiRorNJtJAdxguu5TMvBHOc/F370KvqAe9t48vYp+/TbPKRNrMh/J55tOUmkuIqstZaew==", + "dev": true, + "dependencies": { + "esbuild": "^0.14.27", + "postcss": "^8.4.13", + "resolve": "^1.22.0", + "rollup": "^2.59.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": ">=12.2.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + }, + "peerDependencies": { + "less": "*", + "sass": "*", + "stylus": "*" + }, + "peerDependenciesMeta": { + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "stylus": { + "optional": true + } + } + }, + "node_modules/w3c-keyname": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.4.tgz", + "integrity": "sha512-tOhfEwEzFLJzf6d1ZPkYfGj+FWhIpBux9ppoP3rlclw3Z0BZv3N7b7030Z1kYth+6rDuAsXUFr+d0VE6Ed1ikw==" + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/which-boxed-primitive": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz", + "integrity": "sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==", + "dev": true, + "dependencies": { + "is-bigint": "^1.0.1", + "is-boolean-object": "^1.1.0", + "is-number-object": "^1.0.4", + "is-string": "^1.0.5", + "is-symbol": "^1.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-module": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz", + "integrity": "sha512-B+enWhmw6cjfVC7kS8Pj9pCrKSc5txArRyaYGe088shv/FGWH+0Rjx/xPgtsWfsUtS27FkP697E4DDhgrgoc0Q==" + }, + "node_modules/word-wrap": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", + "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/wrap-ansi/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" + }, + "node_modules/xstate": { + "version": "4.32.1", + "resolved": "https://registry.npmjs.org/xstate/-/xstate-4.32.1.tgz", + "integrity": "sha512-QYUd+3GkXZ8i6qdixnOn28bL3EvA++LONYL/EMWwKlFSh/hiLndJ8YTnz77FDs+JUXcwU7NZJg7qoezoRHc4GQ==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/xstate" + } + }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "dev": true, + "engines": { + "node": ">=0.4" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, + "node_modules/yaml": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", + "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", + "engines": { + "node": ">= 6" + } + }, + "node_modules/yargs": { + "version": "17.5.1", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.5.1.tgz", + "integrity": "sha512-t6YAJcxDkNX7NFYiVtKvWUz8l+PaKTLiL63mJYWR2GnHq2gjEWISzsLp9wg3aY36dY1j+gfIEL3pIF+XlJJfbA==", + "dev": true, + "dependencies": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.0.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.0.1.tgz", + "integrity": "sha512-9BK1jFpLzJROCI5TzwZL/TU4gqjK5xiHV/RfWLOahrjAko/e4DJkRDZQXfvqAsiZzzYhgAzbgz6lg48jcm4GLg==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/yup": { + "version": "0.32.11", + "resolved": "https://registry.npmjs.org/yup/-/yup-0.32.11.tgz", + "integrity": "sha512-Z2Fe1bn+eLstG8DRR6FTavGD+MeAwyfmouhHsIUgaADz8jvFKbO/fXc2trJKZg+5EBjh4gGm3iU/t3onKlXHIg==", + "dependencies": { + "@babel/runtime": "^7.15.4", + "@types/lodash": "^4.14.175", + "lodash": "^4.17.21", + "lodash-es": "^4.17.21", + "nanoclone": "^0.2.1", + "property-expr": "^2.0.4", + "toposort": "^2.0.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/zustand": { + "version": "4.3.8", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-4.3.8.tgz", + "integrity": "sha512-4h28KCkHg5ii/wcFFJ5Fp+k1J3gJoasaIbppdgZFO4BPJnsNxL0mQXBSFgOgAdCdBj35aDTPvdAJReTMntFPGg==", + "dependencies": { + "use-sync-external-store": "1.2.0" + }, + "engines": { + "node": ">=12.7.0" + }, + "peerDependencies": { + "immer": ">=9.0", + "react": ">=16.8" + }, + "peerDependenciesMeta": { + "immer": { + "optional": true + }, + "react": { + "optional": true + } + } + } + }, + "dependencies": { + "@ampproject/remapping": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.0.tgz", + "integrity": "sha512-qRmjj8nj9qmLTQXXmaR1cck3UXSRMPrbsLJAasZpF+t3riI71BXed5ebIOYwQntykeZuhjsdweEc9BxH5Jc26w==", + "dev": true, + "requires": { + "@jridgewell/gen-mapping": "^0.1.0", + "@jridgewell/trace-mapping": "^0.3.9" + } + }, + "@babel/code-frame": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.16.7.tgz", + "integrity": "sha512-iAXqUn8IIeBTNd72xsFlgaXHkMBMt6y4HJp1tIaK465CWLT/fG1aqB7ykr95gHHmlBdGbFeWWfyB4NJJ0nmeIg==", + "requires": { + "@babel/highlight": "^7.16.7" + } + }, + "@babel/compat-data": { + "version": "7.18.5", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.18.5.tgz", + "integrity": "sha512-BxhE40PVCBxVEJsSBhB6UWyAuqJRxGsAw8BdHMJ3AKGydcwuWW4kOO3HmqBQAdcq/OP+/DlTVxLvsCzRTnZuGg==", + "dev": true + }, + "@babel/core": { + "version": "7.18.5", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.18.5.tgz", + "integrity": "sha512-MGY8vg3DxMnctw0LdvSEojOsumc70g0t18gNyUdAZqB1Rpd1Bqo/svHGvt+UJ6JcGX+DIekGFDxxIWofBxLCnQ==", + "dev": true, + "requires": { + "@ampproject/remapping": "^2.1.0", + "@babel/code-frame": "^7.16.7", + "@babel/generator": "^7.18.2", + "@babel/helper-compilation-targets": "^7.18.2", + "@babel/helper-module-transforms": "^7.18.0", + "@babel/helpers": "^7.18.2", + "@babel/parser": "^7.18.5", + "@babel/template": "^7.16.7", + "@babel/traverse": "^7.18.5", + "@babel/types": "^7.18.4", + "convert-source-map": "^1.7.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.1", + "semver": "^6.3.0" + }, + "dependencies": { + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true + } + } + }, + "@babel/generator": { + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.18.2.tgz", + "integrity": "sha512-W1lG5vUwFvfMd8HVXqdfbuG7RuaSrTCCD8cl8fP8wOivdbtbIg2Db3IWUcgvfxKbbn6ZBGYRW/Zk1MIwK49mgw==", + "dev": true, + "requires": { + "@babel/types": "^7.18.2", + "@jridgewell/gen-mapping": "^0.3.0", + "jsesc": "^2.5.1" + }, + "dependencies": { + "@jridgewell/gen-mapping": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.1.tgz", + "integrity": "sha512-GcHwniMlA2z+WFPWuY8lp3fsza0I8xPFMWL5+n8LYyP6PSvPrXf4+n8stDHZY2DM0zy9sVkRDy1jDI4XGzYVqg==", + "dev": true, + "requires": { + "@jridgewell/set-array": "^1.0.0", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.9" + } + } + } + }, + "@babel/helper-annotate-as-pure": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.16.7.tgz", + "integrity": "sha512-s6t2w/IPQVTAET1HitoowRGXooX8mCgtuP5195wD/QJPV6wYjpujCGF7JuMODVX2ZAJOf1GT6DT9MHEZvLOFSw==", + "dev": true, + "requires": { + "@babel/types": "^7.16.7" + } + }, + "@babel/helper-compilation-targets": { + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.18.2.tgz", + "integrity": "sha512-s1jnPotJS9uQnzFtiZVBUxe67CuBa679oWFHpxYYnTpRL/1ffhyX44R9uYiXoa/pLXcY9H2moJta0iaanlk/rQ==", + "dev": true, + "requires": { + "@babel/compat-data": "^7.17.10", + "@babel/helper-validator-option": "^7.16.7", + "browserslist": "^4.20.2", + "semver": "^6.3.0" + }, + "dependencies": { + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true + } + } + }, + "@babel/helper-environment-visitor": { + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.18.2.tgz", + "integrity": "sha512-14GQKWkX9oJzPiQQ7/J36FTXcD4kSp8egKjO9nINlSKiHITRA9q/R74qu8S9xlc/b/yjsJItQUeeh3xnGN0voQ==", + "dev": true + }, + "@babel/helper-function-name": { + "version": "7.17.9", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.17.9.tgz", + "integrity": "sha512-7cRisGlVtiVqZ0MW0/yFB4atgpGLWEHUVYnb448hZK4x+vih0YO5UoS11XIYtZYqHd0dIPMdUSv8q5K4LdMnIg==", + "dev": true, + "requires": { + "@babel/template": "^7.16.7", + "@babel/types": "^7.17.0" + } + }, + "@babel/helper-hoist-variables": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.16.7.tgz", + "integrity": "sha512-m04d/0Op34H5v7pbZw6pSKP7weA6lsMvfiIAMeIvkY/R4xQtBSMFEigu9QTZ2qB/9l22vsxtM8a+Q8CzD255fg==", + "dev": true, + "requires": { + "@babel/types": "^7.16.7" + } + }, + "@babel/helper-module-imports": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.16.7.tgz", + "integrity": "sha512-LVtS6TqjJHFc+nYeITRo6VLXve70xmq7wPhWTqDJusJEgGmkAACWwMiTNrvfoQo6hEhFwAIixNkvB0jPXDL8Wg==", + "requires": { + "@babel/types": "^7.16.7" + } + }, + "@babel/helper-module-transforms": { + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.18.0.tgz", + "integrity": "sha512-kclUYSUBIjlvnzN2++K9f2qzYKFgjmnmjwL4zlmU5f8ZtzgWe8s0rUPSTGy2HmK4P8T52MQsS+HTQAgZd3dMEA==", + "dev": true, + "requires": { + "@babel/helper-environment-visitor": "^7.16.7", + "@babel/helper-module-imports": "^7.16.7", + "@babel/helper-simple-access": "^7.17.7", + "@babel/helper-split-export-declaration": "^7.16.7", + "@babel/helper-validator-identifier": "^7.16.7", + "@babel/template": "^7.16.7", + "@babel/traverse": "^7.18.0", + "@babel/types": "^7.18.0" + } + }, + "@babel/helper-plugin-utils": { + "version": "7.17.12", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.17.12.tgz", + "integrity": "sha512-JDkf04mqtN3y4iAbO1hv9U2ARpPyPL1zqyWs/2WG1pgSq9llHFjStX5jdxb84himgJm+8Ng+x0oiWF/nw/XQKA==", + "dev": true + }, + "@babel/helper-simple-access": { + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.18.2.tgz", + "integrity": "sha512-7LIrjYzndorDY88MycupkpQLKS1AFfsVRm2k/9PtKScSy5tZq0McZTj+DiMRynboZfIqOKvo03pmhTaUgiD6fQ==", + "dev": true, + "requires": { + "@babel/types": "^7.18.2" + } + }, + "@babel/helper-split-export-declaration": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.16.7.tgz", + "integrity": "sha512-xbWoy/PFoxSWazIToT9Sif+jJTlrMcndIsaOKvTA6u7QEo7ilkRZpjew18/W3c7nm8fXdUDXh02VXTbZ0pGDNw==", + "dev": true, + "requires": { + "@babel/types": "^7.16.7" + } + }, + "@babel/helper-validator-identifier": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.16.7.tgz", + "integrity": "sha512-hsEnFemeiW4D08A5gUAZxLBTXpZ39P+a+DGDsHw1yxqyQ/jzFEnxf5uTEGp+3bzAbNOxU1paTgYS4ECU/IgfDw==" + }, + "@babel/helper-validator-option": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.16.7.tgz", + "integrity": "sha512-TRtenOuRUVo9oIQGPC5G9DgK4743cdxvtOw0weQNpZXaS16SCBi5MNjZF8vba3ETURjZpTbVn7Vvcf2eAwFozQ==", + "dev": true + }, + "@babel/helpers": { + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.18.2.tgz", + "integrity": "sha512-j+d+u5xT5utcQSzrh9p+PaJX94h++KN+ng9b9WEJq7pkUPAd61FGqhjuUEdfknb3E/uDBb7ruwEeKkIxNJPIrg==", + "dev": true, + "requires": { + "@babel/template": "^7.16.7", + "@babel/traverse": "^7.18.2", + "@babel/types": "^7.18.2" + } + }, + "@babel/highlight": { + "version": "7.17.12", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.17.12.tgz", + "integrity": "sha512-7yykMVF3hfZY2jsHZEEgLc+3x4o1O+fYyULu11GynEUQNwB6lua+IIQn1FiJxNucd5UlyJryrwsOh8PL9Sn8Qg==", + "requires": { + "@babel/helper-validator-identifier": "^7.16.7", + "chalk": "^2.0.0", + "js-tokens": "^4.0.0" + } + }, + "@babel/parser": { + "version": "7.18.5", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.18.5.tgz", + "integrity": "sha512-YZWVaglMiplo7v8f1oMQ5ZPQr0vn7HPeZXxXWsxXJRjGVrzUFn9OxFQl1sb5wzfootjA/yChhW84BV+383FSOw==", + "dev": true + }, + "@babel/plugin-syntax-jsx": { + "version": "7.17.12", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.17.12.tgz", + "integrity": "sha512-spyY3E3AURfxh/RHtjx5j6hs8am5NbUBGfcZ2vB3uShSpZdQyXSf5rR5Mk76vbtlAZOelyVQ71Fg0x9SG4fsog==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.17.12" + } + }, + "@babel/plugin-transform-react-jsx": { + "version": "7.17.12", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.17.12.tgz", + "integrity": "sha512-Lcaw8bxd1DKht3thfD4A12dqo1X16he1Lm8rIv8sTwjAYNInRS1qHa9aJoqvzpscItXvftKDCfaEQzwoVyXpEQ==", + "dev": true, + "requires": { + "@babel/helper-annotate-as-pure": "^7.16.7", + "@babel/helper-module-imports": "^7.16.7", + "@babel/helper-plugin-utils": "^7.17.12", + "@babel/plugin-syntax-jsx": "^7.17.12", + "@babel/types": "^7.17.12" + } + }, + "@babel/plugin-transform-react-jsx-development": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-development/-/plugin-transform-react-jsx-development-7.16.7.tgz", + "integrity": "sha512-RMvQWvpla+xy6MlBpPlrKZCMRs2AGiHOGHY3xRwl0pEeim348dDyxeH4xBsMPbIMhujeq7ihE702eM2Ew0Wo+A==", + "dev": true, + "requires": { + "@babel/plugin-transform-react-jsx": "^7.16.7" + } + }, + "@babel/plugin-transform-react-jsx-self": { + "version": "7.17.12", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.17.12.tgz", + "integrity": "sha512-7S9G2B44EnYOx74mue02t1uD8ckWZ/ee6Uz/qfdzc35uWHX5NgRy9i+iJSb2LFRgMd+QV9zNcStQaazzzZ3n3Q==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.17.12" + } + }, + "@babel/plugin-transform-react-jsx-source": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.16.7.tgz", + "integrity": "sha512-rONFiQz9vgbsnaMtQlZCjIRwhJvlrPET8TabIUK2hzlXw9B9s2Ieaxte1SCOOXMbWRHodbKixNf3BLcWVOQ8Bw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.16.7" + } + }, + "@babel/runtime": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.22.5.tgz", + "integrity": "sha512-ecjvYlnAaZ/KVneE/OdKYBYfgXV3Ptu6zQWmgEF7vwKhQnvVS6bjMD2XYgj+SNvQ1GfK/pjgokfPkC/2CO8CuA==", + "requires": { + "regenerator-runtime": "^0.13.11" + } + }, + "@babel/template": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.16.7.tgz", + "integrity": "sha512-I8j/x8kHUrbYRTUxXrrMbfCa7jxkE7tZre39x3kjr9hvI82cK1FfqLygotcWN5kdPGWcLdWMHpSBavse5tWw3w==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.16.7", + "@babel/parser": "^7.16.7", + "@babel/types": "^7.16.7" + } + }, + "@babel/traverse": { + "version": "7.18.5", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.18.5.tgz", + "integrity": "sha512-aKXj1KT66sBj0vVzk6rEeAO6Z9aiiQ68wfDgge3nHhA/my6xMM/7HGQUNumKZaoa2qUPQ5whJG9aAifsxUKfLA==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.16.7", + "@babel/generator": "^7.18.2", + "@babel/helper-environment-visitor": "^7.18.2", + "@babel/helper-function-name": "^7.17.9", + "@babel/helper-hoist-variables": "^7.16.7", + "@babel/helper-split-export-declaration": "^7.16.7", + "@babel/parser": "^7.18.5", + "@babel/types": "^7.18.4", + "debug": "^4.1.0", + "globals": "^11.1.0" + } + }, + "@babel/types": { + "version": "7.18.4", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.18.4.tgz", + "integrity": "sha512-ThN1mBcMq5pG/Vm2IcBmPPfyPXbd8S02rS+OBIDENdufvqC7Z/jHPCv9IcP01277aKtDI8g/2XysBN4hA8niiw==", + "requires": { + "@babel/helper-validator-identifier": "^7.16.7", + "to-fast-properties": "^2.0.0" + } + }, + "@codemirror/autocomplete": { + "version": "0.19.15", + "resolved": "https://registry.npmjs.org/@codemirror/autocomplete/-/autocomplete-0.19.15.tgz", + "integrity": "sha512-GQWzvvuXxNUyaEk+5gawbAD8s51/v2Chb++nx0e2eGWrphWk42isBtzOMdc3DxrxrZtPZ55q2ldNp+6G8KJLIQ==", + "requires": { + "@codemirror/language": "^0.19.0", + "@codemirror/state": "^0.19.4", + "@codemirror/text": "^0.19.2", + "@codemirror/tooltip": "^0.19.12", + "@codemirror/view": "^0.19.0", + "@lezer/common": "^0.15.0" + } + }, + "@codemirror/basic-setup": { + "version": "0.19.3", + "resolved": "https://registry.npmjs.org/@codemirror/basic-setup/-/basic-setup-0.19.3.tgz", + "integrity": "sha512-2hfO+QDk/HTpQzeYk1NyL1G9D5L7Sj78dtaQP8xBU42DKU9+OBPF5MdjLYnxP0jKzm6IfQfsLd89fnqW3rBVfQ==", + "requires": { + "@codemirror/autocomplete": "^0.19.0", + "@codemirror/closebrackets": "^0.19.0", + "@codemirror/commands": "^0.19.0", + "@codemirror/comment": "^0.19.0", + "@codemirror/fold": "^0.19.0", + "@codemirror/gutter": "^0.19.0", + "@codemirror/highlight": "^0.19.0", + "@codemirror/history": "^0.19.0", + "@codemirror/language": "^0.19.0", + "@codemirror/lint": "^0.19.0", + "@codemirror/matchbrackets": "^0.19.0", + "@codemirror/rectangular-selection": "^0.19.2", + "@codemirror/search": "^0.19.0", + "@codemirror/state": "^0.19.0", + "@codemirror/view": "^0.19.31" + } + }, + "@codemirror/closebrackets": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/@codemirror/closebrackets/-/closebrackets-0.19.2.tgz", + "integrity": "sha512-ClMPzPcPP0eQiDcVjtVPl6OLxgdtZSYDazsvT0AKl70V1OJva0eHgl4/6kCW3RZ0pb2n34i9nJz4eXCmK+TYDA==", + "requires": { + "@codemirror/language": "^0.19.0", + "@codemirror/rangeset": "^0.19.0", + "@codemirror/state": "^0.19.2", + "@codemirror/text": "^0.19.0", + "@codemirror/view": "^0.19.44" + } + }, + "@codemirror/commands": { + "version": "0.19.8", + "resolved": "https://registry.npmjs.org/@codemirror/commands/-/commands-0.19.8.tgz", + "integrity": "sha512-65LIMSGUGGpY3oH6mzV46YWRrgao6NmfJ+AuC7jNz3K5NPnH6GCV1H5I6SwOFyVbkiygGyd0EFwrWqywTBD1aw==", + "requires": { + "@codemirror/language": "^0.19.0", + "@codemirror/matchbrackets": "^0.19.0", + "@codemirror/state": "^0.19.2", + "@codemirror/text": "^0.19.6", + "@codemirror/view": "^0.19.22", + "@lezer/common": "^0.15.0" + } + }, + "@codemirror/comment": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@codemirror/comment/-/comment-0.19.1.tgz", + "integrity": "sha512-uGKteBuVWAC6fW+Yt8u27DOnXMT/xV4Ekk2Z5mRsiADCZDqYvryrJd6PLL5+8t64BVyocwQwNfz1UswYS2CtFQ==", + "requires": { + "@codemirror/state": "^0.19.9", + "@codemirror/text": "^0.19.0", + "@codemirror/view": "^0.19.0" + } + }, + "@codemirror/fold": { + "version": "0.19.4", + "resolved": "https://registry.npmjs.org/@codemirror/fold/-/fold-0.19.4.tgz", + "integrity": "sha512-0SNSkRSOa6gymD6GauHa3sxiysjPhUC0SRVyTlvL52o0gz9GHdc8kNqNQskm3fBtGGOiSriGwF/kAsajRiGhVw==", + "requires": { + "@codemirror/gutter": "^0.19.0", + "@codemirror/language": "^0.19.0", + "@codemirror/rangeset": "^0.19.0", + "@codemirror/state": "^0.19.0", + "@codemirror/view": "^0.19.22" + } + }, + "@codemirror/gutter": { + "version": "0.19.9", + "resolved": "https://registry.npmjs.org/@codemirror/gutter/-/gutter-0.19.9.tgz", + "integrity": "sha512-PFrtmilahin1g6uL27aG5tM/rqR9DZzZYZsIrCXA5Uc2OFTFqx4owuhoU9hqfYxHp5ovfvBwQ+txFzqS4vog6Q==", + "requires": { + "@codemirror/rangeset": "^0.19.0", + "@codemirror/state": "^0.19.0", + "@codemirror/view": "^0.19.23" + } + }, + "@codemirror/highlight": { + "version": "0.19.8", + "resolved": "https://registry.npmjs.org/@codemirror/highlight/-/highlight-0.19.8.tgz", + "integrity": "sha512-v/lzuHjrYR8MN2mEJcUD6fHSTXXli9C1XGYpr+ElV6fLBIUhMTNKR3qThp611xuWfXfwDxeL7ppcbkM/MzPV3A==", + "requires": { + "@codemirror/language": "^0.19.0", + "@codemirror/rangeset": "^0.19.0", + "@codemirror/state": "^0.19.3", + "@codemirror/view": "^0.19.39", + "@lezer/common": "^0.15.0", + "style-mod": "^4.0.0" + } + }, + "@codemirror/history": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/@codemirror/history/-/history-0.19.2.tgz", + "integrity": "sha512-unhP4t3N2smzmHoo/Yio6ueWi+il8gm9VKrvi6wlcdGH5fOfVDNkmjHQ495SiR+EdOG35+3iNebSPYww0vN7ow==", + "requires": { + "@codemirror/state": "^0.19.2", + "@codemirror/view": "^0.19.0" + } + }, + "@codemirror/lang-cpp": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@codemirror/lang-cpp/-/lang-cpp-0.19.1.tgz", + "integrity": "sha512-BGvZkfcqcalAwxocuE9DhH6gqflm5IjL/8mGTzc8bHzeP1N4innK8qo2G69ohEML4LDZv4WyXc3y4C9/zsGCGQ==", + "requires": { + "@codemirror/highlight": "^0.19.0", + "@codemirror/language": "^0.19.0", + "@lezer/cpp": "^0.15.0" + } + }, + "@codemirror/lang-css": { + "version": "0.19.3", + "resolved": "https://registry.npmjs.org/@codemirror/lang-css/-/lang-css-0.19.3.tgz", + "integrity": "sha512-tyCUJR42/UlfOPLb94/p7dN+IPsYSIzHbAHP2KQHANj0I+Orqp+IyIOS++M8TuCX4zkWh9dvi8s92yy/Tn8Ifg==", + "requires": { + "@codemirror/autocomplete": "^0.19.0", + "@codemirror/highlight": "^0.19.6", + "@codemirror/language": "^0.19.0", + "@codemirror/state": "^0.19.0", + "@lezer/css": "^0.15.2" + } + }, + "@codemirror/lang-html": { + "version": "0.19.4", + "resolved": "https://registry.npmjs.org/@codemirror/lang-html/-/lang-html-0.19.4.tgz", + "integrity": "sha512-GpiEikNuCBeFnS+/TJSeanwqaOfNm8Kkp9WpVNEPZCLyW1mAMCuFJu/3xlWYeWc778Hc3vJqGn3bn+cLNubgCA==", + "requires": { + "@codemirror/autocomplete": "^0.19.0", + "@codemirror/highlight": "^0.19.6", + "@codemirror/lang-css": "^0.19.0", + "@codemirror/lang-javascript": "^0.19.0", + "@codemirror/language": "^0.19.0", + "@codemirror/state": "^0.19.0", + "@lezer/common": "^0.15.0", + "@lezer/html": "^0.15.0" + } + }, + "@codemirror/lang-java": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@codemirror/lang-java/-/lang-java-0.19.1.tgz", + "integrity": "sha512-yA3kcW2GgY0mC2a9dE+uRxGxPWeykfE/GqEPk4TSmhuU4ndmyDgM5QQP7pgnYSZmv2vKoyf4x7NMg8AF7lKXHQ==", + "requires": { + "@codemirror/highlight": "^0.19.0", + "@codemirror/language": "^0.19.0", + "@lezer/java": "^0.15.0" + } + }, + "@codemirror/lang-javascript": { + "version": "0.19.7", + "resolved": "https://registry.npmjs.org/@codemirror/lang-javascript/-/lang-javascript-0.19.7.tgz", + "integrity": "sha512-DL9f3JLqOEHH9cIwEqqjnP5bkjdVXeECksLtV+/MbPm+l4H+AG+PkwZaJQ2oR1GfPZKh8MVSIE94aGWNkJP8WQ==", + "requires": { + "@codemirror/autocomplete": "^0.19.0", + "@codemirror/highlight": "^0.19.7", + "@codemirror/language": "^0.19.0", + "@codemirror/lint": "^0.19.0", + "@codemirror/state": "^0.19.0", + "@codemirror/view": "^0.19.0", + "@lezer/javascript": "^0.15.1" + } + }, + "@codemirror/lang-json": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/@codemirror/lang-json/-/lang-json-0.19.2.tgz", + "integrity": "sha512-fgUWR58Is59P5D/tiazX6oTczioOCDYqjFT5PEBAmLBFMSsRqcnJE0xNO1snrhg7pWEFDq5wR/oN0eZhkeR6Gg==", + "requires": { + "@codemirror/highlight": "^0.19.0", + "@codemirror/language": "^0.19.0", + "@lezer/json": "^0.15.0" + } + }, + "@codemirror/lang-markdown": { + "version": "0.19.6", + "resolved": "https://registry.npmjs.org/@codemirror/lang-markdown/-/lang-markdown-0.19.6.tgz", + "integrity": "sha512-ojoHeLgv1Rfu0GNGsU0bCtXAIp5dy4VKjndHScITQdlCkS/+SAIfuoeowEx+nMAQwTxI+/9fQZ3xdZVznGFYug==", + "requires": { + "@codemirror/highlight": "^0.19.0", + "@codemirror/lang-html": "^0.19.0", + "@codemirror/language": "^0.19.0", + "@codemirror/state": "^0.19.3", + "@codemirror/view": "^0.19.0", + "@lezer/common": "^0.15.0", + "@lezer/markdown": "^0.15.0" + } + }, + "@codemirror/lang-php": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@codemirror/lang-php/-/lang-php-0.19.1.tgz", + "integrity": "sha512-Q6djLACHu1J6XbnxWlEPCiyqqDrlZLi9QtjY6b9vqdkq/GOsNaXVv44nDY8DD6Bxi5yYRTJ3yh8XzsKuJgztjQ==", + "requires": { + "@codemirror/highlight": "^0.19.0", + "@codemirror/lang-html": "^0.19.0", + "@codemirror/language": "^0.19.0", + "@codemirror/state": "^0.19.0", + "@lezer/common": "^0.15.0", + "@lezer/php": "^0.15.0" + } + }, + "@codemirror/lang-python": { + "version": "0.19.5", + "resolved": "https://registry.npmjs.org/@codemirror/lang-python/-/lang-python-0.19.5.tgz", + "integrity": "sha512-MQf7t0k6+i9KCzlFCI8EY+jjwyXLy5AwjmXsMyMCMbOw/97j70jFZYrs7Mm7RJakNE2rypWhnLGlyBTSYMqR5g==", + "requires": { + "@codemirror/highlight": "^0.19.7", + "@codemirror/language": "^0.19.0", + "@lezer/python": "^0.15.0" + } + }, + "@codemirror/lang-rust": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/@codemirror/lang-rust/-/lang-rust-0.19.2.tgz", + "integrity": "sha512-SEXsO7Qf2gktRvVhHMc0Mq4HzPBpFcQlrlcinafy6VFXavWs+QAIB8UAuLG/igOc3PrIHbZFlyEhVUIGstox8w==", + "requires": { + "@codemirror/highlight": "^0.19.7", + "@codemirror/language": "^0.19.0", + "@lezer/rust": "^0.15.0" + } + }, + "@codemirror/lang-sql": { + "version": "0.19.4", + "resolved": "https://registry.npmjs.org/@codemirror/lang-sql/-/lang-sql-0.19.4.tgz", + "integrity": "sha512-4FqLC8aNe1iCDyAWbJmSqa8K7rgz2xTwW36V35z4oiyLoyOLsCayKIwoQqp5DNIq2ckGCsyzotgxXKpgtg/pgg==", + "requires": { + "@codemirror/autocomplete": "^0.19.0", + "@codemirror/highlight": "^0.19.0", + "@codemirror/language": "^0.19.0", + "@codemirror/state": "^0.19.0", + "@lezer/lr": "^0.15.0" + } + }, + "@codemirror/lang-wast": { + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/@codemirror/lang-wast/-/lang-wast-0.19.0.tgz", + "integrity": "sha512-mr/Bp4k8+fJ0P8/Q6L45pnX7/bDBk4VP8ahYrTdvHo+UaOqBBhBFtBqBikvX8ZDQiUTfuZ4tnJE2QtOvmFsuzg==", + "requires": { + "@codemirror/highlight": "^0.19.0", + "@codemirror/language": "^0.19.0", + "@lezer/lr": "^0.15.0" + } + }, + "@codemirror/lang-xml": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/@codemirror/lang-xml/-/lang-xml-0.19.2.tgz", + "integrity": "sha512-9VIjxvqcH1sk8bmYbxQon0lXhVZgdHdfjGes+e4Akgvb43aMBDNvIQVALwrCb+XMEHTxLUMQtrsBN0G64yCUXw==", + "requires": { + "@codemirror/autocomplete": "^0.19.0", + "@codemirror/highlight": "^0.19.6", + "@codemirror/language": "^0.19.0", + "@codemirror/state": "^0.19.0", + "@lezer/common": "^0.15.0", + "@lezer/xml": "^0.15.0" + } + }, + "@codemirror/language": { + "version": "0.19.10", + "resolved": "https://registry.npmjs.org/@codemirror/language/-/language-0.19.10.tgz", + "integrity": "sha512-yA0DZ3RYn2CqAAGW62VrU8c4YxscMQn45y/I9sjBlqB1e2OTQLg4CCkMBuMSLXk4xaqjlsgazeOQWaJQOKfV8Q==", + "requires": { + "@codemirror/state": "^0.19.0", + "@codemirror/text": "^0.19.0", + "@codemirror/view": "^0.19.0", + "@lezer/common": "^0.15.5", + "@lezer/lr": "^0.15.0" + } + }, + "@codemirror/language-data": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/@codemirror/language-data/-/language-data-0.19.2.tgz", + "integrity": "sha512-O38TaBfzqs5vK8Z+ZlAmaGqciQxgtAXacOTSq22ZLrsKmYMbeFZNHCqDL6VMG2wOt1jtRnfJD56chONwaPRUVQ==", + "requires": { + "@codemirror/lang-cpp": "^0.19.0", + "@codemirror/lang-css": "^0.19.0", + "@codemirror/lang-html": "^0.19.0", + "@codemirror/lang-java": "^0.19.0", + "@codemirror/lang-javascript": "^0.19.0", + "@codemirror/lang-json": "^0.19.0", + "@codemirror/lang-markdown": "^0.19.0", + "@codemirror/lang-php": "^0.19.0", + "@codemirror/lang-python": "^0.19.0", + "@codemirror/lang-rust": "^0.19.0", + "@codemirror/lang-sql": "^0.19.0", + "@codemirror/lang-wast": "^0.19.0", + "@codemirror/lang-xml": "^0.19.0", + "@codemirror/language": "^0.19.0", + "@codemirror/legacy-modes": "^0.19.0", + "@codemirror/stream-parser": "^0.19.0" + } + }, + "@codemirror/legacy-modes": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@codemirror/legacy-modes/-/legacy-modes-0.19.1.tgz", + "integrity": "sha512-vYPLsD/ON+3SXhlGj9Qb3fpFNNU3Ya/AtDiv/g3OyqVzhh5vs5rAnOvk8xopGWRwppdhlNPD9VyXjiOmZUQtmQ==", + "requires": { + "@codemirror/stream-parser": "^0.19.0" + } + }, + "@codemirror/lint": { + "version": "0.19.6", + "resolved": "https://registry.npmjs.org/@codemirror/lint/-/lint-0.19.6.tgz", + "integrity": "sha512-Pbw1Y5kHVs2J+itQ0uez3dI4qY9ApYVap7eNfV81x1/3/BXgBkKfadaw0gqJ4h4FDG7OnJwb0VbPsjJQllHjaA==", + "requires": { + "@codemirror/gutter": "^0.19.4", + "@codemirror/panel": "^0.19.0", + "@codemirror/rangeset": "^0.19.1", + "@codemirror/state": "^0.19.4", + "@codemirror/tooltip": "^0.19.16", + "@codemirror/view": "^0.19.22", + "crelt": "^1.0.5" + } + }, + "@codemirror/matchbrackets": { + "version": "0.19.4", + "resolved": "https://registry.npmjs.org/@codemirror/matchbrackets/-/matchbrackets-0.19.4.tgz", + "integrity": "sha512-VFkaOKPNudAA5sGP1zikRHCEKU0hjYmkKpr04pybUpQvfTvNJXlReCyP0rvH/1iEwAGPL990ZTT+QrLdu4MeEA==", + "requires": { + "@codemirror/language": "^0.19.0", + "@codemirror/state": "^0.19.0", + "@codemirror/view": "^0.19.0", + "@lezer/common": "^0.15.0" + } + }, + "@codemirror/panel": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@codemirror/panel/-/panel-0.19.1.tgz", + "integrity": "sha512-sYeOCMA3KRYxZYJYn5PNlt9yNsjy3zTNTrbYSfVgjgL9QomIVgOJWPO5hZ2sTN8lufO6lw0vTBsIPL9MSidmBg==", + "requires": { + "@codemirror/state": "^0.19.0", + "@codemirror/view": "^0.19.0" + } + }, + "@codemirror/rangeset": { + "version": "0.19.9", + "resolved": "https://registry.npmjs.org/@codemirror/rangeset/-/rangeset-0.19.9.tgz", + "integrity": "sha512-V8YUuOvK+ew87Xem+71nKcqu1SXd5QROMRLMS/ljT5/3MCxtgrRie1Cvild0G/Z2f1fpWxzX78V0U4jjXBorBQ==", + "requires": { + "@codemirror/state": "^0.19.0" + } + }, + "@codemirror/rectangular-selection": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/@codemirror/rectangular-selection/-/rectangular-selection-0.19.2.tgz", + "integrity": "sha512-AXK/p5eGwFJ9GJcLfntqN4dgY+XiIF7eHfXNQJX5HhQLSped2wJE6WuC1rMEaOlcpOqlb9mrNi/ZdUjSIj9mbA==", + "requires": { + "@codemirror/state": "^0.19.0", + "@codemirror/text": "^0.19.4", + "@codemirror/view": "^0.19.48" + } + }, + "@codemirror/search": { + "version": "0.19.10", + "resolved": "https://registry.npmjs.org/@codemirror/search/-/search-0.19.10.tgz", + "integrity": "sha512-qjubm69HJixPBWzI6HeEghTWOOD8NXiHOTRNvdizqs8xWRuFChq9zkjD3XiAJ7GXSTzCuQJnAP9DBBGCLq4ZIA==", + "requires": { + "@codemirror/panel": "^0.19.0", + "@codemirror/rangeset": "^0.19.0", + "@codemirror/state": "^0.19.3", + "@codemirror/text": "^0.19.0", + "@codemirror/view": "^0.19.34", + "crelt": "^1.0.5" + } + }, + "@codemirror/state": { + "version": "0.19.9", + "resolved": "https://registry.npmjs.org/@codemirror/state/-/state-0.19.9.tgz", + "integrity": "sha512-psOzDolKTZkx4CgUqhBQ8T8gBc0xN5z4gzed109aF6x7D7umpDRoimacI/O6d9UGuyl4eYuDCZmDFr2Rq7aGOw==", + "requires": { + "@codemirror/text": "^0.19.0" + } + }, + "@codemirror/stream-parser": { + "version": "0.19.9", + "resolved": "https://registry.npmjs.org/@codemirror/stream-parser/-/stream-parser-0.19.9.tgz", + "integrity": "sha512-WTmkEFSRCetpk8xIOvV2yyXdZs3DgYckM0IP7eFi4ewlxWnJO/H4BeJZLs4wQaydWsAqTQoDyIwNH1BCzK5LUQ==", + "requires": { + "@codemirror/highlight": "^0.19.0", + "@codemirror/language": "^0.19.0", + "@codemirror/state": "^0.19.0", + "@codemirror/text": "^0.19.0", + "@lezer/common": "^0.15.0", + "@lezer/lr": "^0.15.0" + } + }, + "@codemirror/text": { + "version": "0.19.6", + "resolved": "https://registry.npmjs.org/@codemirror/text/-/text-0.19.6.tgz", + "integrity": "sha512-T9jnREMIygx+TPC1bOuepz18maGq/92q2a+n4qTqObKwvNMg+8cMTslb8yxeEDEq7S3kpgGWxgO1UWbQRij0dA==" + }, + "@codemirror/theme-one-dark": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@codemirror/theme-one-dark/-/theme-one-dark-0.19.1.tgz", + "integrity": "sha512-8gc4c2k2o/EhyHoWkghCxp5vyDT96JaFGtRy35PHwIom0LZdx7aU4AbDUnITvwiFB+0+i54VO+WQjBqgTyJvqg==", + "requires": { + "@codemirror/highlight": "^0.19.0", + "@codemirror/state": "^0.19.0", + "@codemirror/view": "^0.19.0" + } + }, + "@codemirror/tooltip": { + "version": "0.19.16", + "resolved": "https://registry.npmjs.org/@codemirror/tooltip/-/tooltip-0.19.16.tgz", + "integrity": "sha512-zxKDHryUV5/RS45AQL+wOeN+i7/l81wK56OMnUPoTSzCWNITfxHn7BToDsjtrRKbzHqUxKYmBnn/4hPjpZ4WJQ==", + "requires": { + "@codemirror/state": "^0.19.0", + "@codemirror/view": "^0.19.0" + } + }, + "@codemirror/view": { + "version": "0.19.48", + "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-0.19.48.tgz", + "integrity": "sha512-0eg7D2Nz4S8/caetCTz61rK0tkHI17V/d15Jy0kLOT8dTLGGNJUponDnW28h2B6bERmPlVHKh8MJIr5OCp1nGw==", + "requires": { + "@codemirror/rangeset": "^0.19.5", + "@codemirror/state": "^0.19.3", + "@codemirror/text": "^0.19.0", + "style-mod": "^4.0.0", + "w3c-keyname": "^2.2.4" + } + }, + "@date-io/core": { + "version": "2.14.0", + "resolved": "https://registry.npmjs.org/@date-io/core/-/core-2.14.0.tgz", + "integrity": "sha512-qFN64hiFjmlDHJhu+9xMkdfDG2jLsggNxKXglnekUpXSq8faiqZgtHm2lsHCUuaPDTV6wuXHcCl8J1GQ5wLmPw==" + }, + "@date-io/date-fns": { + "version": "2.14.0", + "resolved": "https://registry.npmjs.org/@date-io/date-fns/-/date-fns-2.14.0.tgz", + "integrity": "sha512-4fJctdVyOd5cKIKGaWUM+s3MUXMuzkZaHuTY15PH70kU1YTMrCoauA7hgQVx9qj0ZEbGrH9VSPYJYnYro7nKiA==", + "requires": { + "@date-io/core": "^2.14.0" + } + }, + "@date-io/dayjs": { + "version": "2.14.0", + "resolved": "https://registry.npmjs.org/@date-io/dayjs/-/dayjs-2.14.0.tgz", + "integrity": "sha512-4fRvNWaOh7AjvOyJ4h6FYMS7VHLQnIEeAV5ahv6sKYWx+1g1UwYup8h7+gPuoF+sW2hTScxi7PVaba2Jk/U8Og==", + "requires": { + "@date-io/core": "^2.14.0" + } + }, + "@date-io/luxon": { + "version": "2.14.0", + "resolved": "https://registry.npmjs.org/@date-io/luxon/-/luxon-2.14.0.tgz", + "integrity": "sha512-KmpBKkQFJ/YwZgVd0T3h+br/O0uL9ZdE7mn903VPAG2ZZncEmaUfUdYKFT7v7GyIKJ4KzCp379CRthEbxevEVg==", + "requires": { + "@date-io/core": "^2.14.0" + } + }, + "@date-io/moment": { + "version": "2.14.0", + "resolved": "https://registry.npmjs.org/@date-io/moment/-/moment-2.14.0.tgz", + "integrity": "sha512-VsoLXs94GsZ49ecWuvFbsa081zEv2xxG7d+izJsqGa2L8RPZLlwk27ANh87+SNnOUpp+qy2AoCAf0mx4XXhioA==", + "requires": { + "@date-io/core": "^2.14.0" + } + }, + "@emotion/babel-plugin": { + "version": "11.11.0", + "resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.11.0.tgz", + "integrity": "sha512-m4HEDZleaaCH+XgDDsPF15Ht6wTLsgDTeR3WYj9Q/k76JtWhrJjcP4+/XlG8LGT/Rol9qUfOIztXeA84ATpqPQ==", + "requires": { + "@babel/helper-module-imports": "^7.16.7", + "@babel/runtime": "^7.18.3", + "@emotion/hash": "^0.9.1", + "@emotion/memoize": "^0.8.1", + "@emotion/serialize": "^1.1.2", + "babel-plugin-macros": "^3.1.0", + "convert-source-map": "^1.5.0", + "escape-string-regexp": "^4.0.0", + "find-root": "^1.1.0", + "source-map": "^0.5.7", + "stylis": "4.2.0" + }, + "dependencies": { + "escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==" + }, + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==" + } + } + }, + "@emotion/cache": { + "version": "11.11.0", + "resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-11.11.0.tgz", + "integrity": "sha512-P34z9ssTCBi3e9EI1ZsWpNHcfY1r09ZO0rZbRO2ob3ZQMnFI35jB536qoXbkdesr5EUhYi22anuEJuyxifaqAQ==", + "requires": { + "@emotion/memoize": "^0.8.1", + "@emotion/sheet": "^1.2.2", + "@emotion/utils": "^1.2.1", + "@emotion/weak-memoize": "^0.3.1", + "stylis": "4.2.0" + } + }, + "@emotion/hash": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.1.tgz", + "integrity": "sha512-gJB6HLm5rYwSLI6PQa+X1t5CFGrv1J1TWG+sOyMCeKz2ojaj6Fnl/rZEspogG+cvqbt4AE/2eIyD2QfLKTBNlQ==" + }, + "@emotion/is-prop-valid": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.2.1.tgz", + "integrity": "sha512-61Mf7Ufx4aDxx1xlDeOm8aFFigGHE4z+0sKCa+IHCeZKiyP9RLD0Mmx7m8b9/Cf37f7NAvQOOJAbQQGVr5uERw==", + "requires": { + "@emotion/memoize": "^0.8.1" + } + }, + "@emotion/memoize": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.8.1.tgz", + "integrity": "sha512-W2P2c/VRW1/1tLox0mVUalvnWXxavmv/Oum2aPsRcoDJuob75FC3Y8FbpfLwUegRcxINtGUMPq0tFCvYNTBXNA==" + }, + "@emotion/react": { + "version": "11.11.1", + "resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.11.1.tgz", + "integrity": "sha512-5mlW1DquU5HaxjLkfkGN1GA/fvVGdyHURRiX/0FHl2cfIfRxSOfmxEH5YS43edp0OldZrZ+dkBKbngxcNCdZvA==", + "requires": { + "@babel/runtime": "^7.18.3", + "@emotion/babel-plugin": "^11.11.0", + "@emotion/cache": "^11.11.0", + "@emotion/serialize": "^1.1.2", + "@emotion/use-insertion-effect-with-fallbacks": "^1.0.1", + "@emotion/utils": "^1.2.1", + "@emotion/weak-memoize": "^0.3.1", + "hoist-non-react-statics": "^3.3.1" + } + }, + "@emotion/serialize": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.1.2.tgz", + "integrity": "sha512-zR6a/fkFP4EAcCMQtLOhIgpprZOwNmCldtpaISpvz348+DP4Mz8ZoKaGGCQpbzepNIUWbq4w6hNZkwDyKoS+HA==", + "requires": { + "@emotion/hash": "^0.9.1", + "@emotion/memoize": "^0.8.1", + "@emotion/unitless": "^0.8.1", + "@emotion/utils": "^1.2.1", + "csstype": "^3.0.2" + } + }, + "@emotion/sheet": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@emotion/sheet/-/sheet-1.2.2.tgz", + "integrity": "sha512-0QBtGvaqtWi+nx6doRwDdBIzhNdZrXUppvTM4dtZZWEGTXL/XE/yJxLMGlDT1Gt+UHH5IX1n+jkXyytE/av7OA==" + }, + "@emotion/styled": { + "version": "11.11.0", + "resolved": "https://registry.npmjs.org/@emotion/styled/-/styled-11.11.0.tgz", + "integrity": "sha512-hM5Nnvu9P3midq5aaXj4I+lnSfNi7Pmd4EWk1fOZ3pxookaQTNew6bp4JaCBYM4HVFZF9g7UjJmsUmC2JlxOng==", + "requires": { + "@babel/runtime": "^7.18.3", + "@emotion/babel-plugin": "^11.11.0", + "@emotion/is-prop-valid": "^1.2.1", + "@emotion/serialize": "^1.1.2", + "@emotion/use-insertion-effect-with-fallbacks": "^1.0.1", + "@emotion/utils": "^1.2.1" + } + }, + "@emotion/unitless": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.8.1.tgz", + "integrity": "sha512-KOEGMu6dmJZtpadb476IsZBclKvILjopjUii3V+7MnXIQCYh8W3NgNcgwo21n9LXZX6EDIKvqfjYxXebDwxKmQ==" + }, + "@emotion/use-insertion-effect-with-fallbacks": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@emotion/use-insertion-effect-with-fallbacks/-/use-insertion-effect-with-fallbacks-1.0.1.tgz", + "integrity": "sha512-jT/qyKZ9rzLErtrjGgdkMBn2OP8wl0G3sQlBb3YPryvKHsjvINUhVaPFfP+fpBcOkmrVOVEEHQFJ7nbj2TH2gw==", + "requires": {} + }, + "@emotion/utils": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-1.2.1.tgz", + "integrity": "sha512-Y2tGf3I+XVnajdItskUCn6LX+VUDmP6lTL4fcqsXAv43dnlbZiuW4MWQW38rW/BVWSE7Q/7+XQocmpnRYILUmg==" + }, + "@emotion/weak-memoize": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.3.1.tgz", + "integrity": "sha512-EsBwpc7hBUJWAsNPBmJy4hxWx12v6bshQsldrVmjxJoc3isbxhOrF2IcCpaXxfvq03NwkI7sbsOLXbYuqF/8Ww==" + }, + "@eslint/eslintrc": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.3.0.tgz", + "integrity": "sha512-UWW0TMTmk2d7hLcWD1/e2g5HDM/HQ3csaLSqXCfqwh4uNDuNqlaKWXmEsL4Cs41Z0KnILNvwbHAah3C2yt06kw==", + "dev": true, + "requires": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^9.3.2", + "globals": "^13.15.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "dependencies": { + "globals": { + "version": "13.15.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.15.0.tgz", + "integrity": "sha512-bpzcOlgDhMG070Av0Vy5Owklpv1I6+j96GhUI7Rh7IzDCKLzboflLrrfqMu8NquDbiR4EOQk7XzJwqVJxicxog==", + "dev": true, + "requires": { + "type-fest": "^0.20.2" + } + } + } + }, + "@floating-ui/core": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.3.1.tgz", + "integrity": "sha512-Bu+AMaXNjrpjh41znzHqaz3r2Nr8hHuHZT6V2LBKMhyMl0FgKA62PNYbqnfgmzOhoWZj70Zecisbo4H1rotP5g==" + }, + "@floating-ui/dom": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.4.1.tgz", + "integrity": "sha512-loCXUOLzIC3jp50RFOKXZ/kQjjz26ryr/23M+FWG9jrmAv8lRf3DUfC2AiVZ3+K316GOhB08CR+Povwz8e9mDw==", + "requires": { + "@floating-ui/core": "^1.3.1" + } + }, + "@fortawesome/fontawesome-free": { + "version": "5.15.4", + "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-free/-/fontawesome-free-5.15.4.tgz", + "integrity": "sha512-eYm8vijH/hpzr/6/1CJ/V/Eb1xQFW2nnUKArb3z+yUWv7HTwj6M7SP957oMjfZjAHU6qpoNc2wQvIxBLWYa/Jg==" + }, + "@hookform/resolvers": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-2.4.0.tgz", + "integrity": "sha512-KiHc7Uwd2IJMvPTMQ9vQxfss2ulq2gRYL/HYZ90qiTs+07UgGWCikiIvE2pKjjGVltEYjq5eR8x0ITmoyEjGxQ==", + "requires": {} + }, + "@humanwhocodes/config-array": { + "version": "0.9.5", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.9.5.tgz", + "integrity": "sha512-ObyMyWxZiCu/yTisA7uzx81s40xR2fD5Cg/2Kq7G02ajkNubJf6BopgDTmDyc3U7sXpNKM8cYOw7s7Tyr+DnCw==", + "dev": true, + "requires": { + "@humanwhocodes/object-schema": "^1.2.1", + "debug": "^4.1.1", + "minimatch": "^3.0.4" + } + }, + "@humanwhocodes/object-schema": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz", + "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==", + "dev": true + }, + "@jest/types": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-26.6.2.tgz", + "integrity": "sha512-fC6QCp7Sc5sX6g8Tvbmj4XUTbyrik0akgRy03yjXbQaBWWNWGE7SGtJk98m0N8nzegD/7SggrUlivxo5ax4KWQ==", + "dev": true, + "requires": { + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^15.0.0", + "chalk": "^4.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "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" + } + } + } + }, + "@jridgewell/gen-mapping": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.1.1.tgz", + "integrity": "sha512-sQXCasFk+U8lWYEe66WxRDOE9PjVz4vSM51fTu3Hw+ClTpUSQb718772vH3pyS5pShp6lvQM7SxgIDXXXmOX7w==", + "dev": true, + "requires": { + "@jridgewell/set-array": "^1.0.0", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "@jridgewell/resolve-uri": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.0.7.tgz", + "integrity": "sha512-8cXDaBBHOr2pQ7j77Y6Vp5VDT2sIqWyWQ56TjEq4ih/a4iST3dItRe8Q9fp0rrIl9DoKhWQtUQz/YpOxLkXbNA==", + "dev": true + }, + "@jridgewell/set-array": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.1.tgz", + "integrity": "sha512-Ct5MqZkLGEXTVmQYbGtx9SVqD2fqwvdubdps5D3djjAkgkKwT918VNOz65pEHFaYTeWcukmJmH5SwsA9Tn2ObQ==", + "dev": true + }, + "@jridgewell/sourcemap-codec": { + "version": "1.4.13", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.13.tgz", + "integrity": "sha512-GryiOJmNcWbovBxTfZSF71V/mXbgcV3MewDe3kIMCLyIh5e7SKAeUZs+rMnJ8jkMolZ/4/VsdBmMrw3l+VdZ3w==", + "dev": true + }, + "@jridgewell/trace-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.13.tgz", + "integrity": "sha512-o1xbKhp9qnIAoHJSWd6KlCZfqslL4valSF81H8ImioOAxluWYWOpWkpyktY2vnt4tbrX9XYaxovq6cgowaJp2w==", + "dev": true, + "requires": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "@lezer/common": { + "version": "0.15.12", + "resolved": "https://registry.npmjs.org/@lezer/common/-/common-0.15.12.tgz", + "integrity": "sha512-edfwCxNLnzq5pBA/yaIhwJ3U3Kz8VAUOTRg0hhxaizaI1N+qxV7EXDv/kLCkLeq2RzSFvxexlaj5Mzfn2kY0Ig==" + }, + "@lezer/cpp": { + "version": "0.15.3", + "resolved": "https://registry.npmjs.org/@lezer/cpp/-/cpp-0.15.3.tgz", + "integrity": "sha512-QE5YxhnoQ4eJH9G2h5r+m4Zq7d/0NmA0eAnZmiOVggI7a3jpODIXZeJbkUPf4U2yzNCSWAGpZVk8XxkA+cTZvA==", + "requires": { + "@lezer/lr": "^0.15.0" + } + }, + "@lezer/css": { + "version": "0.15.2", + "resolved": "https://registry.npmjs.org/@lezer/css/-/css-0.15.2.tgz", + "integrity": "sha512-tnMOMZY0Zs6JQeVjqfmREYMV0GnmZR1NitndLWioZMD6mA7VQF/PPKPmJX1f+ZgVZQc5Am0df9mX3aiJnNJlKQ==", + "requires": { + "@lezer/lr": "^0.15.0" + } + }, + "@lezer/html": { + "version": "0.15.1", + "resolved": "https://registry.npmjs.org/@lezer/html/-/html-0.15.1.tgz", + "integrity": "sha512-0ZYVhu+RwN6ZMM0gNnTxenRAdoycKc2wvpLfMjP0JkKR0vMxhtuLaIpsq9KW2Mv6l7ux5vdjq8CQ7fKDvia8KA==", + "requires": { + "@lezer/lr": "^0.15.0" + } + }, + "@lezer/java": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/@lezer/java/-/java-0.15.0.tgz", + "integrity": "sha512-Od2Ugo93XjLxCIEKlrwJfacmSMd7lEnkVQgBjMsZofjwEKZ2Y2ue6URntMFFiftTlNXbE29vYbweWYluEq+Cdw==", + "requires": { + "@lezer/lr": "^0.15.0" + } + }, + "@lezer/javascript": { + "version": "0.15.3", + "resolved": "https://registry.npmjs.org/@lezer/javascript/-/javascript-0.15.3.tgz", + "integrity": "sha512-8jA2NpOfpWwSPZxRhd9BxK2ZPvGd7nLE3LFTJ5AbMhXAzMHeMjneV6GEVd7dAIee85dtap0jdb6bgOSO0+lfwA==", + "requires": { + "@lezer/lr": "^0.15.0" + } + }, + "@lezer/json": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/@lezer/json/-/json-0.15.0.tgz", + "integrity": "sha512-OsMjjBkTkeQ15iMCu5U1OiBubRC4V9Wm03zdIlUgNZ20aUPx5DWDRqUc5wG41JXVSj7Lxmo+idlFCfBBdxB8sw==", + "requires": { + "@lezer/lr": "^0.15.0" + } + }, + "@lezer/lr": { + "version": "0.15.8", + "resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-0.15.8.tgz", + "integrity": "sha512-bM6oE6VQZ6hIFxDNKk8bKPa14hqFrV07J/vHGOeiAbJReIaQXmkVb6xQu4MR+JBTLa5arGRyAAjJe1qaQt3Uvg==", + "requires": { + "@lezer/common": "^0.15.0" + } + }, + "@lezer/markdown": { + "version": "0.15.6", + "resolved": "https://registry.npmjs.org/@lezer/markdown/-/markdown-0.15.6.tgz", + "integrity": "sha512-1XXLa4q0ZthryUEfO47ipvZHxNb+sCKoQIMM9dKs5vXZOBbgF2Vah/GL3g26BFIAEc2uCv4VQnI+lSrv58BT3g==", + "requires": { + "@lezer/common": "^0.15.0" + } + }, + "@lezer/php": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/@lezer/php/-/php-0.15.0.tgz", + "integrity": "sha512-kU3QSOko0jsv3RLhABPrRD4wEhaWYh2Uh0lTj9Q9BOsBJ5SoADfifO4gHkEDav7AgL/j+ulkKiHiilciTa/RaQ==", + "requires": { + "@lezer/lr": "^0.15.0" + } + }, + "@lezer/python": { + "version": "0.15.1", + "resolved": "https://registry.npmjs.org/@lezer/python/-/python-0.15.1.tgz", + "integrity": "sha512-Xdb2nh+FoxR8ssEADGsroDtsnP+EDhiPpW9zhER3h+6cpGtZ2e9Oq/Rwn9nFQRiKCfMT+AQaqC3ZgAbhbnumyQ==", + "requires": { + "@lezer/lr": "^0.15.0" + } + }, + "@lezer/rust": { + "version": "0.15.1", + "resolved": "https://registry.npmjs.org/@lezer/rust/-/rust-0.15.1.tgz", + "integrity": "sha512-9R7Mcfe/XWodpT7bYNKoOmEAN+AOHHfma9QUTdEhqduzd1G4qsdQkGSMPfsqt24sZCkQ1EREbE/lmEp4YxTlcA==", + "requires": { + "@lezer/lr": "^0.15.0" + } + }, + "@lezer/xml": { + "version": "0.15.1", + "resolved": "https://registry.npmjs.org/@lezer/xml/-/xml-0.15.1.tgz", + "integrity": "sha512-vVh01enxM9hSGOcFtztmX+Pa460HDq5jIeft9bDCe17PUOU0nAbfo883I3cW9lUOcmWNQ3btbkmXMGjRszJE6g==", + "requires": { + "@lezer/lr": "^0.15.0" + } + }, + "@maif/react-forms": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/@maif/react-forms/-/react-forms-1.6.3.tgz", + "integrity": "sha512-dMEuffnbPS7m7YM0bJVGajmpTSdHzrngdF8pM/RIPst//X0hjD/1HWveG9qAW5/MC2JIKlBrN7w6IpOdJmTBIg==", + "requires": { + "@codemirror/basic-setup": "^0.19.1", + "@codemirror/lang-html": "^0.19.4", + "@codemirror/lang-javascript": "^0.19.7", + "@codemirror/lang-json": "^0.19.2", + "@codemirror/lang-markdown": "^0.19.6", + "@codemirror/language-data": "^0.19.2", + "@codemirror/theme-one-dark": "^0.19.1", + "@emotion/react": "^11.9.3", + "@emotion/styled": "^11.9.3", + "@fortawesome/fontawesome-free": "^5.15.3", + "@hookform/resolvers": "2.4.0", + "@mui/material": "^5.8.7", + "@mui/system": "^5.8.7", + "@mui/x-date-pickers": "^5.0.0-alpha.7", + "classnames": "2.3.0", + "date-fns": "^2.28.0", + "fast-deep-equal": "^3.1.3", + "highlight.js": "^11.5.1", + "lodash.debounce": "4.0.8", + "moment": "2.29.4", + "object-hash": "3.0.0", + "react-feather": "2.0.9", + "react-hook-form": "^7.33.1", + "react-select": "5.2.1", + "react-tooltip": "4.2.21", + "showdown": "1.9.1", + "uuid": "8.3.2", + "yup": "0.32.11" + }, + "dependencies": { + "classnames": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.3.0.tgz", + "integrity": "sha512-UUf/S3eeczXBjHPpSnrZ1ZyxH3KmLW8nVYFUWIZA/dixYMIQr7l94yYKxaAkmPk7HO9dlT6gFqAPZC02tTdfQw==" + }, + "react-dom": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-17.0.2.tgz", + "integrity": "sha512-s4h96KtLDUQlsENhMn1ar8t2bEa+q/YAtj8pPPdIjPDGBDIVNsrD9aXNWqspUe6AzKCIG0C1HZZLqLV7qpOBGA==", + "peer": true, + "requires": { + "loose-envify": "^1.1.0", + "object-assign": "^4.1.1", + "scheduler": "^0.20.2" + } + }, + "react-feather": { + "version": "2.0.9", + "resolved": "https://registry.npmjs.org/react-feather/-/react-feather-2.0.9.tgz", + "integrity": "sha512-yMfCGRkZdXwIs23Zw/zIWCJO3m3tlaUvtHiXlW+3FH7cIT6fiK1iJ7RJWugXq7Fso8ZaQyUm92/GOOHXvkiVUw==", + "requires": { + "prop-types": "^15.7.2" + } + }, + "react-select": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/react-select/-/react-select-5.2.1.tgz", + "integrity": "sha512-OOyNzfKrhOcw/BlembyGWgdlJ2ObZRaqmQppPFut1RptJO423j+Y+JIsmxkvsZ4D/3CpOmwIlCvWbbAWEdh12A==", + "requires": { + "@babel/runtime": "^7.12.0", + "@emotion/cache": "^11.4.0", + "@emotion/react": "^11.1.1", + "@types/react-transition-group": "^4.4.0", + "memoize-one": "^5.0.0", + "prop-types": "^15.6.0", + "react-transition-group": "^4.3.0" + } + }, + "react-tooltip": { + "version": "4.2.21", + "resolved": "https://registry.npmjs.org/react-tooltip/-/react-tooltip-4.2.21.tgz", + "integrity": "sha512-zSLprMymBDowknr0KVDiJ05IjZn9mQhhg4PRsqln0OZtURAJ1snt1xi5daZfagsh6vfsziZrc9pErPTDY1ACig==", + "requires": { + "prop-types": "^15.7.2", + "uuid": "^7.0.3" + }, + "dependencies": { + "uuid": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-7.0.3.tgz", + "integrity": "sha512-DPSke0pXhTZgoF/d+WSt2QaKMCFSfx7QegxEWT+JOuHF5aWrKEn0G+ztjuJg/gG8/ItK+rbPCD/yNv8yyih6Cg==" + } + } + }, + "scheduler": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.20.2.tgz", + "integrity": "sha512-2eWfGgAqqWFGqtdMmcL5zCMK1U8KlXv8SQFGglL3CEtd0aDVDWgeF/YoCmvln55m5zSk3J/20hTaSBeSObsQDQ==", + "peer": true, + "requires": { + "loose-envify": "^1.1.0", + "object-assign": "^4.1.1" + } + } + } + }, + "@mui/base": { + "version": "5.0.0-beta.5", + "resolved": "https://registry.npmjs.org/@mui/base/-/base-5.0.0-beta.5.tgz", + "integrity": "sha512-vy3TWLQYdGNecTaufR4wDNQFV2WEg6wRPi6BVbx6q1vP3K1mbxIn1+XOqOzfYBXjFHvMx0gZAo2TgWbaqfgvAA==", + "requires": { + "@babel/runtime": "^7.22.5", + "@emotion/is-prop-valid": "^1.2.1", + "@mui/types": "^7.2.4", + "@mui/utils": "^5.13.6", + "@popperjs/core": "^2.11.8", + "clsx": "^1.2.1", + "prop-types": "^15.8.1", + "react-is": "^18.2.0" + }, + "dependencies": { + "react-is": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", + "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==" + } + } + }, + "@mui/core-downloads-tracker": { + "version": "5.13.4", + "resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-5.13.4.tgz", + "integrity": "sha512-yFrMWcrlI0TqRN5jpb6Ma9iI7sGTHpytdzzL33oskFHNQ8UgrtPas33Y1K7sWAMwCrr1qbWDrOHLAQG4tAzuSw==" + }, + "@mui/material": { + "version": "5.13.6", + "resolved": "https://registry.npmjs.org/@mui/material/-/material-5.13.6.tgz", + "integrity": "sha512-/c2ZApeQm2sTYdQXjqEnldaBMBcUEiyu2VRS6bS39ZeNaAcCLBQbYocLR46R+f0S5dgpBzB0T4AsOABPOFYZ5Q==", + "requires": { + "@babel/runtime": "^7.22.5", + "@mui/base": "5.0.0-beta.5", + "@mui/core-downloads-tracker": "^5.13.4", + "@mui/system": "^5.13.6", + "@mui/types": "^7.2.4", + "@mui/utils": "^5.13.6", + "@types/react-transition-group": "^4.4.6", + "clsx": "^1.2.1", + "csstype": "^3.1.2", + "prop-types": "^15.8.1", + "react-is": "^18.2.0", + "react-transition-group": "^4.4.5" + }, + "dependencies": { + "react-is": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", + "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==" + } + } + }, + "@mui/private-theming": { + "version": "5.13.1", + "resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-5.13.1.tgz", + "integrity": "sha512-HW4npLUD9BAkVppOUZHeO1FOKUJWAwbpy0VQoGe3McUYTlck1HezGHQCfBQ5S/Nszi7EViqiimECVl9xi+/WjQ==", + "requires": { + "@babel/runtime": "^7.21.0", + "@mui/utils": "^5.13.1", + "prop-types": "^15.8.1" + } + }, + "@mui/styled-engine": { + "version": "5.13.2", + "resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-5.13.2.tgz", + "integrity": "sha512-VCYCU6xVtXOrIN8lcbuPmoG+u7FYuOERG++fpY74hPpEWkyFQG97F+/XfTQVYzlR2m7nPjnwVUgATcTCMEaMvw==", + "requires": { + "@babel/runtime": "^7.21.0", + "@emotion/cache": "^11.11.0", + "csstype": "^3.1.2", + "prop-types": "^15.8.1" + } + }, + "@mui/system": { + "version": "5.13.6", + "resolved": "https://registry.npmjs.org/@mui/system/-/system-5.13.6.tgz", + "integrity": "sha512-G3Xr28uLqU3DyF6r2LQkHGw/ku4P0AHzlKVe7FGXOPl7X1u+hoe2xxj8Vdiq/69II/mh9OP21i38yBWgWb7WgQ==", + "requires": { + "@babel/runtime": "^7.22.5", + "@mui/private-theming": "^5.13.1", + "@mui/styled-engine": "^5.13.2", + "@mui/types": "^7.2.4", + "@mui/utils": "^5.13.6", + "clsx": "^1.2.1", + "csstype": "^3.1.2", + "prop-types": "^15.8.1" + } + }, + "@mui/types": { + "version": "7.2.4", + "resolved": "https://registry.npmjs.org/@mui/types/-/types-7.2.4.tgz", + "integrity": "sha512-LBcwa8rN84bKF+f5sDyku42w1NTxaPgPyYKODsh01U1fVstTClbUoSA96oyRBnSNyEiAVjKm6Gwx9vjR+xyqHA==", + "requires": {} + }, + "@mui/utils": { + "version": "5.13.6", + "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-5.13.6.tgz", + "integrity": "sha512-ggNlxl5NPSbp+kNcQLmSig6WVB0Id+4gOxhx644987v4fsji+CSXc+MFYLocFB/x4oHtzCUlSzbVHlJfP/fXoQ==", + "requires": { + "@babel/runtime": "^7.22.5", + "@types/prop-types": "^15.7.5", + "@types/react-is": "^18.2.0", + "prop-types": "^15.8.1", + "react-is": "^18.2.0" + }, + "dependencies": { + "react-is": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", + "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==" + } + } + }, + "@mui/x-date-pickers": { + "version": "5.0.0-beta.0", + "resolved": "https://registry.npmjs.org/@mui/x-date-pickers/-/x-date-pickers-5.0.0-beta.0.tgz", + "integrity": "sha512-WfcYe+5j3xbGO9d+uMFem06b9q+9yIcFj0dP3PKCa1zb6m3Tbkigig6vlCuHLKLSXe1P6IQCt+BNVVbU1rfh7A==", + "requires": { + "@babel/runtime": "^7.17.2", + "@date-io/core": "^2.14.0", + "@date-io/date-fns": "^2.14.0", + "@date-io/dayjs": "^2.14.0", + "@date-io/luxon": "^2.14.0", + "@date-io/moment": "^2.14.0", + "@mui/utils": "^5.4.1", + "@types/react-transition-group": "^4.4.4", + "clsx": "^1.2.1", + "prop-types": "^15.7.2", + "react-transition-group": "^4.4.2", + "rifm": "^0.12.1" + } + }, + "@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "requires": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + } + }, + "@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true + }, + "@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "requires": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + } + }, + "@playwright/test": { + "version": "1.41.1", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.41.1.tgz", + "integrity": "sha512-9g8EWTjiQ9yFBXc6HjCWe41msLpxEX0KhmfmPl9RPLJdfzL4F0lg2BdJ91O9azFdl11y1pmpwdjBiSxvqc+btw==", + "dev": true, + "requires": { + "playwright": "1.41.1" + } + }, + "@popperjs/core": { + "version": "2.11.8", + "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", + "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==" + }, + "@remix-run/router": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.0.0.tgz", + "integrity": "sha512-SCR1cxRSMNKjaVYptCzBApPDqGwa3FGdjVHc+rOToocNPHQdIYLZBfv/3f+KvYuXDkUGVIW9IAzmPNZDRL1I4A==" + }, + "@rollup/pluginutils": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-4.2.1.tgz", + "integrity": "sha512-iKnFXr7NkdZAIHiIWE+BX5ULi/ucVFYWD6TbAV+rZctiRTY2PL6tsIKhoIOaoskiWAkgu+VsbXgUVDNLHf+InQ==", + "dev": true, + "requires": { + "estree-walker": "^2.0.1", + "picomatch": "^2.2.2" + } + }, + "@tanstack/react-table": { + "version": "8.1.3", + "resolved": "https://registry.npmjs.org/@tanstack/react-table/-/react-table-8.1.3.tgz", + "integrity": "sha512-rgGb4Sou8kuJI2NuJbDSS/wRc+TVmXZPg5+vslHZqA+tLvHvYgLHndBc6kW2fzCdInBshJEgHAnDXillYGYi+w==", + "requires": { + "@tanstack/table-core": "8.1.2" + } + }, + "@tanstack/table-core": { + "version": "8.1.2", + "resolved": "https://registry.npmjs.org/@tanstack/table-core/-/table-core-8.1.2.tgz", + "integrity": "sha512-h0e9xBC0BRVoQE8w5BVypjPc2x5+H1VcwQDLKdijoVgUpO2S0ixjY9ejZ3YAtPYkBZTukLm9+3wfF4CFUXwD/Q==" + }, + "@textea/json-viewer": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@textea/json-viewer/-/json-viewer-3.1.1.tgz", + "integrity": "sha512-eaYwufnvQhlbdeWdqhERdp4yOKp2bILiMOGKIRBFoFpg3ry6L06GcdNFffe15C8T+2F/vUUEtLVpZ9i5f8NUrw==", + "requires": { + "clsx": "^1.2.1", + "copy-to-clipboard": "^3.3.3", + "zustand": "^4.3.7" + } + }, + "@types/istanbul-lib-coverage": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.4.tgz", + "integrity": "sha512-z/QT1XN4K4KYuslS23k62yDIDLwLFkzxOuMplDtObz0+y7VqJCaO2o+SPwHCvLFZh7xazvvoor2tA/hPz9ee7g==", + "dev": true + }, + "@types/istanbul-lib-report": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz", + "integrity": "sha512-plGgXAPfVKFoYfa9NpYDAkseG+g6Jr294RqeqcqDixSbU34MZVJRi/P+7Y8GDpzkEwLaGZZOpKIEmeVZNtKsrg==", + "dev": true, + "requires": { + "@types/istanbul-lib-coverage": "*" + } + }, + "@types/istanbul-reports": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.1.tgz", + "integrity": "sha512-c3mAZEuK0lvBp8tmuL74XRKn1+y2dcwOUpH7x4WrF6gk1GIgiluDRgMYQtw2OFcBvAJWlt6ASU3tSqxp0Uu0Aw==", + "dev": true, + "requires": { + "@types/istanbul-lib-report": "*" + } + }, + "@types/jest": { + "version": "26.0.24", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-26.0.24.tgz", + "integrity": "sha512-E/X5Vib8BWqZNRlDxj9vYXhsDwPYbPINqKF9BsnSoon4RQ0D9moEuLD8txgyypFLH7J4+Lho9Nr/c8H0Fi+17w==", + "dev": true, + "requires": { + "jest-diff": "^26.0.0", + "pretty-format": "^26.0.0" + } + }, + "@types/json-schema": { + "version": "7.0.11", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.11.tgz", + "integrity": "sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==", + "dev": true + }, + "@types/lodash": { + "version": "4.14.182", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.182.tgz", + "integrity": "sha512-/THyiqyQAP9AfARo4pF+aCGcyiQ94tX/Is2I7HofNRqoYLgN1PBoOWu2/zTA5zMxzP5EFutMtWtGAFRKUe961Q==" + }, + "@types/node": { + "version": "16.11.41", + "resolved": "https://registry.npmjs.org/@types/node/-/node-16.11.41.tgz", + "integrity": "sha512-mqoYK2TnVjdkGk8qXAVGc/x9nSaTpSrFaGFm43BUH3IdoBV0nta6hYaGmdOvIMlbHJbUEVen3gvwpwovAZKNdQ==", + "dev": true + }, + "@types/parse-json": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.0.tgz", + "integrity": "sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==" + }, + "@types/prop-types": { + "version": "15.7.5", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.5.tgz", + "integrity": "sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==" + }, + "@types/react": { + "version": "18.0.13", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.0.13.tgz", + "integrity": "sha512-psqptIYQxGUFuGYwP3KCFVtPTkMpIcrqFmtKblWEUQhLuYLpHBwJkXhjp6eHfDM5IbyskY4x7qQpLedEsPkHlA==", + "requires": { + "@types/prop-types": "*", + "@types/scheduler": "*", + "csstype": "^3.0.2" + } + }, + "@types/react-dom": { + "version": "18.0.5", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.0.5.tgz", + "integrity": "sha512-OWPWTUrY/NIrjsAPkAk1wW9LZeIjSvkXRhclsFO8CZcZGCOg2G0YZy4ft+rOyYxy8B7ui5iZzi9OkDebZ7/QSA==", + "dev": true, + "requires": { + "@types/react": "*" + } + }, + "@types/react-is": { + "version": "18.2.1", + "resolved": "https://registry.npmjs.org/@types/react-is/-/react-is-18.2.1.tgz", + "integrity": "sha512-wyUkmaaSZEzFZivD8F2ftSyAfk6L+DfFliVj/mYdOXbVjRcS87fQJLTnhk6dRZPuJjI+9g6RZJO4PNCngUrmyw==", + "requires": { + "@types/react": "*" + } + }, + "@types/react-transition-group": { + "version": "4.4.6", + "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.6.tgz", + "integrity": "sha512-VnCdSxfcm08KjsJVQcfBmhEQAPnLB8G08hAxn39azX1qYBQ/5RVQuoHuKIcfKOdncuaUvEpFKFzEvbtIMsfVew==", + "requires": { + "@types/react": "*" + } + }, + "@types/scheduler": { + "version": "0.16.2", + "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.2.tgz", + "integrity": "sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew==" + }, + "@types/yargs": { + "version": "15.0.14", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-15.0.14.tgz", + "integrity": "sha512-yEJzHoxf6SyQGhBhIYGXQDSCkJjB6HohDShto7m8vaKg9Yp0Yn8+71J9eakh2bnPg6BfsH9PRMhiRTZnd4eXGQ==", + "dev": true, + "requires": { + "@types/yargs-parser": "*" + } + }, + "@types/yargs-parser": { + "version": "21.0.0", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.0.tgz", + "integrity": "sha512-iO9ZQHkZxHn4mSakYV0vFHAVDyEOIJQrV2uZ06HxEPcx+mt8swXoZHIbaaJ2crJYFfErySgktuTZ3BeLz+XmFA==", + "dev": true + }, + "@typescript-eslint/eslint-plugin": { + "version": "5.28.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.28.0.tgz", + "integrity": "sha512-DXVU6Cg29H2M6EybqSg2A+x8DgO9TCUBRp4QEXQHJceLS7ogVDP0g3Lkg/SZCqcvkAP/RruuQqK0gdlkgmhSUA==", + "dev": true, + "requires": { + "@typescript-eslint/scope-manager": "5.28.0", + "@typescript-eslint/type-utils": "5.28.0", + "@typescript-eslint/utils": "5.28.0", + "debug": "^4.3.4", + "functional-red-black-tree": "^1.0.1", + "ignore": "^5.2.0", + "regexpp": "^3.2.0", + "semver": "^7.3.7", + "tsutils": "^3.21.0" + } + }, + "@typescript-eslint/parser": { + "version": "5.28.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.28.0.tgz", + "integrity": "sha512-ekqoNRNK1lAcKhZESN/PdpVsWbP9jtiNqzFWkp/yAUdZvJalw2heCYuqRmM5eUJSIYEkgq5sGOjq+ZqsLMjtRA==", + "dev": true, + "requires": { + "@typescript-eslint/scope-manager": "5.28.0", + "@typescript-eslint/types": "5.28.0", + "@typescript-eslint/typescript-estree": "5.28.0", + "debug": "^4.3.4" + } + }, + "@typescript-eslint/scope-manager": { + "version": "5.28.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.28.0.tgz", + "integrity": "sha512-LeBLTqF/he1Z+boRhSqnso6YrzcKMTQ8bO/YKEe+6+O/JGof9M0g3IJlIsqfrK/6K03MlFIlycbf1uQR1IjE+w==", + "dev": true, + "requires": { + "@typescript-eslint/types": "5.28.0", + "@typescript-eslint/visitor-keys": "5.28.0" + } + }, + "@typescript-eslint/type-utils": { + "version": "5.28.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.28.0.tgz", + "integrity": "sha512-SyKjKh4CXPglueyC6ceAFytjYWMoPHMswPQae236zqe1YbhvCVQyIawesYywGiu98L9DwrxsBN69vGIVxJ4mQQ==", + "dev": true, + "requires": { + "@typescript-eslint/utils": "5.28.0", + "debug": "^4.3.4", + "tsutils": "^3.21.0" + } + }, + "@typescript-eslint/types": { + "version": "5.28.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.28.0.tgz", + "integrity": "sha512-2OOm8ZTOQxqkPbf+DAo8oc16sDlVR5owgJfKheBkxBKg1vAfw2JsSofH9+16VPlN9PWtv8Wzhklkqw3k/zCVxA==", + "dev": true + }, + "@typescript-eslint/typescript-estree": { + "version": "5.28.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.28.0.tgz", + "integrity": "sha512-9GX+GfpV+F4hdTtYc6OV9ZkyYilGXPmQpm6AThInpBmKJEyRSIjORJd1G9+bknb7OTFYL+Vd4FBJAO6T78OVqA==", + "dev": true, + "requires": { + "@typescript-eslint/types": "5.28.0", + "@typescript-eslint/visitor-keys": "5.28.0", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "semver": "^7.3.7", + "tsutils": "^3.21.0" + } + }, + "@typescript-eslint/utils": { + "version": "5.28.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.28.0.tgz", + "integrity": "sha512-E60N5L0fjv7iPJV3UGc4EC+A3Lcj4jle9zzR0gW7vXhflO7/J29kwiTGITA2RlrmPokKiZbBy2DgaclCaEUs6g==", + "dev": true, + "requires": { + "@types/json-schema": "^7.0.9", + "@typescript-eslint/scope-manager": "5.28.0", + "@typescript-eslint/types": "5.28.0", + "@typescript-eslint/typescript-estree": "5.28.0", + "eslint-scope": "^5.1.1", + "eslint-utils": "^3.0.0" + } + }, + "@typescript-eslint/visitor-keys": { + "version": "5.28.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.28.0.tgz", + "integrity": "sha512-BtfP1vCor8cWacovzzPFOoeW4kBQxzmhxGoOpt0v1SFvG+nJ0cWaVdJk7cky1ArTcFHHKNIxyo2LLr3oNkSuXA==", + "dev": true, + "requires": { + "@typescript-eslint/types": "5.28.0", + "eslint-visitor-keys": "^3.3.0" + } + }, + "@vitejs/plugin-react": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-1.3.2.tgz", + "integrity": "sha512-aurBNmMo0kz1O4qRoY+FM4epSA39y3ShWGuqfLRA/3z0oEJAdtoSfgA3aO98/PCCHAqMaduLxIxErWrVKIFzXA==", + "dev": true, + "requires": { + "@babel/core": "^7.17.10", + "@babel/plugin-transform-react-jsx": "^7.17.3", + "@babel/plugin-transform-react-jsx-development": "^7.16.7", + "@babel/plugin-transform-react-jsx-self": "^7.16.7", + "@babel/plugin-transform-react-jsx-source": "^7.16.7", + "@rollup/pluginutils": "^4.2.1", + "react-refresh": "^0.13.0", + "resolve": "^1.22.0" + } + }, + "@xstate/react": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@xstate/react/-/react-3.0.0.tgz", + "integrity": "sha512-KHSCfwtb8gZ7QH2luihvmKYI+0lcdHQOmGNRUxUEs4zVgaJCyd8csCEmwPsudpliLdUmyxX2pzUBojFkINpotw==", + "requires": { + "use-isomorphic-layout-effect": "^1.0.0", + "use-sync-external-store": "^1.0.0" + } + }, + "acorn": { + "version": "8.7.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.7.1.tgz", + "integrity": "sha512-Xx54uLJQZ19lKygFXOWsscKUbsBZW0CPykPhVQdhIeIwrbPmJzqeASDInc8nKBnp/JT6igTs82qPXz069H8I/A==", + "dev": true + }, + "acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "requires": {} + }, + "ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "requires": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, + "ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true + }, + "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==", + "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" + } + }, + "argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "array-includes": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.5.tgz", + "integrity": "sha512-iSDYZMMyTPkiFasVqfuAQnWAYcvO/SeBSCGKePoEthjp4LEMTe4uLc7b025o4jAZpHhihh8xPo99TNWUWWkGDQ==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "es-abstract": "^1.19.5", + "get-intrinsic": "^1.1.1", + "is-string": "^1.0.7" + } + }, + "array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "dev": true + }, + "array.prototype.flatmap": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.0.tgz", + "integrity": "sha512-PZC9/8TKAIxcWKdyeb77EzULHPrIX/tIZebLJUQOMR1OwYosT8yggdfWScfTBCDj5utONvOuPQQumYsU2ULbkg==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3", + "es-abstract": "^1.19.2", + "es-shim-unscopables": "^1.0.0" + } + }, + "babel-plugin-macros": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/babel-plugin-macros/-/babel-plugin-macros-3.1.0.tgz", + "integrity": "sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==", + "requires": { + "@babel/runtime": "^7.12.5", + "cosmiconfig": "^7.0.0", + "resolve": "^1.19.0" + } + }, + "balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" + }, + "big-integer": { + "version": "1.6.51", + "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.51.tgz", + "integrity": "sha512-GPEid2Y9QU1Exl1rpO9B2IPJGHPSupF5GnVIP0blYvNOMer2bTvSWs1jGOUg04hTmu67nmLsQ9TBo1puaotBHg==" + }, + "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==", + "requires": {} + }, + "bootstrap-icons": { + "version": "1.8.3", + "resolved": "https://registry.npmjs.org/bootstrap-icons/-/bootstrap-icons-1.8.3.tgz", + "integrity": "sha512-s5kmttnbq4BXbx3Bwnj39y+t7Vc3blTtyD77W3aYQ1LlNoS3lNbbGvSYhIbg26Im8KmjScyFpHEevlPOBcIDdA==" + }, + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "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" + } + }, + "broadcast-channel": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/broadcast-channel/-/broadcast-channel-3.7.0.tgz", + "integrity": "sha512-cIAKJXAxGJceNZGTZSBzMxzyOn72cVgPnKx4dc6LRjQgbaJUQqhy5rzL3zbMxkMWsGKkv2hSFkPRMEXfoMZ2Mg==", + "requires": { + "@babel/runtime": "^7.7.2", + "detect-node": "^2.1.0", + "js-sha3": "0.8.0", + "microseconds": "0.2.0", + "nano-time": "1.0.0", + "oblivious-set": "1.0.0", + "rimraf": "3.0.2", + "unload": "2.2.0" + } + }, + "browserslist": { + "version": "4.20.4", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.20.4.tgz", + "integrity": "sha512-ok1d+1WpnU24XYN7oC3QWgTyMhY/avPJ/r9T00xxvUOIparA/gc+UPUMaod3i+G6s+nI2nUb9xZ5k794uIwShw==", + "dev": true, + "requires": { + "caniuse-lite": "^1.0.30001349", + "electron-to-chromium": "^1.4.147", + "escalade": "^3.1.1", + "node-releases": "^2.0.5", + "picocolors": "^1.0.0" + } + }, + "buffer-writer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/buffer-writer/-/buffer-writer-2.0.0.tgz", + "integrity": "sha512-a7ZpuTZU1TRtnwyCNW3I5dc0wWNC3VR9S++Ewyk2HHZdrO3CQJqSpd+95Us590V6AL7JqUAH2IwZ/398PmNFgw==", + "dev": true + }, + "call-bind": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", + "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", + "dev": true, + "requires": { + "function-bind": "^1.1.1", + "get-intrinsic": "^1.0.2" + } + }, + "callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==" + }, + "camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==" + }, + "caniuse-lite": { + "version": "1.0.30001355", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001355.tgz", + "integrity": "sha512-Sd6pjJHF27LzCB7pT7qs+kuX2ndurzCzkpJl6Qct7LPSZ9jn0bkOA8mdgMgmqnQAWLVOOGjLpc+66V57eLtb1g==", + "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==", + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "chokidar": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", + "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", + "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" + }, + "dependencies": { + "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" + } + } + } + }, + "classnames": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.3.1.tgz", + "integrity": "sha512-OlQdbZ7gLfGarSqxesMesDa5uz7KFbID8Kpq/SxIoNGDqY8lSYs0D+hhtBXhcdB3rcbXArFr7vlHheLk1voeNA==" + }, + "cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "dev": true, + "requires": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, + "clsx": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz", + "integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==" + }, + "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==", + "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": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==" + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" + }, + "convert-source-map": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.8.0.tgz", + "integrity": "sha512-+OQdjP49zViI/6i7nIJpA8rAl4sV/JdPfU9nZs3VqOwGIgizICvuN2ru6fMd+4llL0tar18UYJXfZ/TWtmhUjA==", + "requires": { + "safe-buffer": "~5.1.1" + } + }, + "copy-to-clipboard": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/copy-to-clipboard/-/copy-to-clipboard-3.3.3.tgz", + "integrity": "sha512-2KV8NhB5JqC3ky0r9PMCAZKbUHSwtEo4CwCs0KXgruG43gX5PMqDEBbVU4OUzw2MuAWUfsuFmWvEKG5QRfSnJA==", + "requires": { + "toggle-selection": "^1.0.6" + } + }, + "cosmiconfig": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz", + "integrity": "sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==", + "requires": { + "@types/parse-json": "^4.0.0", + "import-fresh": "^3.2.1", + "parse-json": "^5.0.0", + "path-type": "^4.0.0", + "yaml": "^1.10.0" + } + }, + "crelt": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.5.tgz", + "integrity": "sha512-+BO9wPPi+DWTDcNYhr/W90myha8ptzftZT+LwcmUbbok0rcP/fequmFYCw8NMoH7pkAZQzU78b3kYrlua5a9eA==" + }, + "cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, + "requires": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + } + }, + "csstype": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.2.tgz", + "integrity": "sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==" + }, + "date-fns": { + "version": "2.28.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.28.0.tgz", + "integrity": "sha512-8d35hViGYx/QH0icHYCeLmsLmMUheMmTyV9Fcm6gvNwdw31yXXH+O85sOBJ+OLnLQMKZowvpKb6FgMIQjcpvQw==" + }, + "dayjs": { + "version": "1.11.3", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.3.tgz", + "integrity": "sha512-xxwlswWOlGhzgQ4TKzASQkUhqERI3egRNqgV4ScR8wlANA/A9tZ7miXa44vTTKEq5l7vWoL5G57bG3zA+Kow0A==", + "optional": true, + "peer": true + }, + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "requires": { + "ms": "2.1.2" + } + }, + "decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==" + }, + "deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true + }, + "define-lazy-prop": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz", + "integrity": "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==", + "dev": true + }, + "define-properties": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.4.tgz", + "integrity": "sha512-uckOqKcfaVvtBdsVkdPv3XjveQJsNQqmhXgRi8uhvWWuPYZCNlzT8qAyblUgNoXdHdjMTzAqeGjAoli8f+bzPA==", + "dev": true, + "requires": { + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + } + }, + "detect-node": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz", + "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==" + }, + "diff-sequences": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-26.6.2.tgz", + "integrity": "sha512-Mv/TDa3nZ9sbc5soK+OoA74BsS3mL37yixCvUAQkiuA4Wz6YtwP/K47n2rv2ovzHZvoiQeA5FTQOschKkEwB0Q==", + "dev": true + }, + "dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, + "requires": { + "path-type": "^4.0.0" + } + }, + "doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "requires": { + "esutils": "^2.0.2" + } + }, + "dom-helpers": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz", + "integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==", + "requires": { + "@babel/runtime": "^7.8.7", + "csstype": "^3.0.2" + } + }, + "electron-to-chromium": { + "version": "1.4.158", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.158.tgz", + "integrity": "sha512-gppO3/+Y6sP432HtvwvuU8S+YYYLH4PmAYvQwqUtt9HDOmEsBwQfLnK9T8+1NIKwAS1BEygIjTaATC4H5EzvxQ==", + "dev": true + }, + "emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "requires": { + "is-arrayish": "^0.2.1" + } + }, + "es-abstract": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.20.1.tgz", + "integrity": "sha512-WEm2oBhfoI2sImeM4OF2zE2V3BYdSF+KnSi9Sidz51fQHd7+JuF8Xgcj9/0o+OWeIeIS/MiuNnlruQrJf16GQA==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "es-to-primitive": "^1.2.1", + "function-bind": "^1.1.1", + "function.prototype.name": "^1.1.5", + "get-intrinsic": "^1.1.1", + "get-symbol-description": "^1.0.0", + "has": "^1.0.3", + "has-property-descriptors": "^1.0.0", + "has-symbols": "^1.0.3", + "internal-slot": "^1.0.3", + "is-callable": "^1.2.4", + "is-negative-zero": "^2.0.2", + "is-regex": "^1.1.4", + "is-shared-array-buffer": "^1.0.2", + "is-string": "^1.0.7", + "is-weakref": "^1.0.2", + "object-inspect": "^1.12.0", + "object-keys": "^1.1.1", + "object.assign": "^4.1.2", + "regexp.prototype.flags": "^1.4.3", + "string.prototype.trimend": "^1.0.5", + "string.prototype.trimstart": "^1.0.5", + "unbox-primitive": "^1.0.2" + } + }, + "es-shim-unscopables": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.0.0.tgz", + "integrity": "sha512-Jm6GPcCdC30eMLbZ2x8z2WuRwAws3zTBBKuusffYVUrNj/GVSUAZ+xKMaUpfNDR5IbyNA5LJbaecoUVbmUcB1w==", + "dev": true, + "requires": { + "has": "^1.0.3" + } + }, + "es-to-primitive": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", + "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", + "dev": true, + "requires": { + "is-callable": "^1.1.4", + "is-date-object": "^1.0.1", + "is-symbol": "^1.0.2" + } + }, + "esbuild": { + "version": "0.14.44", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.14.44.tgz", + "integrity": "sha512-Rn+lRRfj60r/3svI6NgAVnetzp3vMOj17BThuhshSj/gS1LR03xrjkDYyfPmrYG/0c3D68rC6FNYMQ3yRbiXeQ==", + "dev": true, + "requires": { + "esbuild-android-64": "0.14.44", + "esbuild-android-arm64": "0.14.44", + "esbuild-darwin-64": "0.14.44", + "esbuild-darwin-arm64": "0.14.44", + "esbuild-freebsd-64": "0.14.44", + "esbuild-freebsd-arm64": "0.14.44", + "esbuild-linux-32": "0.14.44", + "esbuild-linux-64": "0.14.44", + "esbuild-linux-arm": "0.14.44", + "esbuild-linux-arm64": "0.14.44", + "esbuild-linux-mips64le": "0.14.44", + "esbuild-linux-ppc64le": "0.14.44", + "esbuild-linux-riscv64": "0.14.44", + "esbuild-linux-s390x": "0.14.44", + "esbuild-netbsd-64": "0.14.44", + "esbuild-openbsd-64": "0.14.44", + "esbuild-sunos-64": "0.14.44", + "esbuild-windows-32": "0.14.44", + "esbuild-windows-64": "0.14.44", + "esbuild-windows-arm64": "0.14.44" + } + }, + "esbuild-android-64": { + "version": "0.14.44", + "resolved": "https://registry.npmjs.org/esbuild-android-64/-/esbuild-android-64-0.14.44.tgz", + "integrity": "sha512-dFPHBXmx385zuJULAD/Cmq/LyPRXiAWbf9ylZtY0wJ8iVyWfKYaCYxeJx8OAZUuj46ZwNa7MzW2GBAQLOeiemg==", + "dev": true, + "optional": true + }, + "esbuild-android-arm64": { + "version": "0.14.44", + "resolved": "https://registry.npmjs.org/esbuild-android-arm64/-/esbuild-android-arm64-0.14.44.tgz", + "integrity": "sha512-qqaqqyxHXjZ/0ddKU3I3Nb7lAvVM69ELMhb8+91FyomAUmQPlHtxe+TTiWxXGHE72XEzcgTEGq4VauqLNkN22g==", + "dev": true, + "optional": true + }, + "esbuild-darwin-64": { + "version": "0.14.44", + "resolved": "https://registry.npmjs.org/esbuild-darwin-64/-/esbuild-darwin-64-0.14.44.tgz", + "integrity": "sha512-RBmtGKGY06+AW6IOJ1LE/dEeF7HH34C1/Ces9FSitU4bIbIpL4KEuQpTFoxwb4ry5s2hyw7vbPhhtyOd18FH9g==", + "dev": true, + "optional": true + }, + "esbuild-darwin-arm64": { + "version": "0.14.44", + "resolved": "https://registry.npmjs.org/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.14.44.tgz", + "integrity": "sha512-Bmhx5Cfo4Hdb7WyyyDupTB8HPmnFZ8baLfPlzLdYvF6OzsIbV+CY+m/AWf0OQvY40BlkzCLJ/7Lfwbb71Tngmg==", + "dev": true, + "optional": true + }, + "esbuild-freebsd-64": { + "version": "0.14.44", + "resolved": "https://registry.npmjs.org/esbuild-freebsd-64/-/esbuild-freebsd-64-0.14.44.tgz", + "integrity": "sha512-O4HpWa5ZgxbNPQTF7URicLzYa+TidGlmGT/RAC3GjbGEQQYkd0R1Slyh69Yrmb2qmcOcPAgWHbNo1UhK4WmZ4w==", + "dev": true, + "optional": true + }, + "esbuild-freebsd-arm64": { + "version": "0.14.44", + "resolved": "https://registry.npmjs.org/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.14.44.tgz", + "integrity": "sha512-f0/jkAKccnDY7mg1F9l/AMzEm+VXWXK6c3IrOEmd13jyKfpTZKTIlt+yI04THPDCDZTzXHTRUBLozqp+m8Mg5Q==", + "dev": true, + "optional": true + }, + "esbuild-linux-32": { + "version": "0.14.44", + "resolved": "https://registry.npmjs.org/esbuild-linux-32/-/esbuild-linux-32-0.14.44.tgz", + "integrity": "sha512-WSIhzLldMR7YUoEL7Ix319tC+NFmW9Pu7NgFWxUfOXeWsT0Wg484hm6bNgs7+oY2pGzg715y/Wrqi1uNOMmZJw==", + "dev": true, + "optional": true + }, + "esbuild-linux-64": { + "version": "0.14.44", + "resolved": "https://registry.npmjs.org/esbuild-linux-64/-/esbuild-linux-64-0.14.44.tgz", + "integrity": "sha512-zgscTrCMcRZRIsVugqBTP/B5lPLNchBlWjQ8sQq2Epnv+UDtYKgXEq1ctWAmibZNy2E9QRCItKMeIEqeTUT5kA==", + "dev": true, + "optional": true + }, + "esbuild-linux-arm": { + "version": "0.14.44", + "resolved": "https://registry.npmjs.org/esbuild-linux-arm/-/esbuild-linux-arm-0.14.44.tgz", + "integrity": "sha512-laPBPwGfsbBxGw6F6jnqic2CPXLyC1bPrmnSOeJ9oEnx1rcKkizd4HWCRUc0xv+l4z/USRfx/sEfYlWSLeqoJQ==", + "dev": true, + "optional": true + }, + "esbuild-linux-arm64": { + "version": "0.14.44", + "resolved": "https://registry.npmjs.org/esbuild-linux-arm64/-/esbuild-linux-arm64-0.14.44.tgz", + "integrity": "sha512-H0H/2/wgiScTwBve/JR8/o+Zhabx5KPk8T2mkYZFKQGl1hpUgC+AOmRyqy/Js3p66Wim4F4Akv3I3sJA1sKg0w==", + "dev": true, + "optional": true + }, + "esbuild-linux-mips64le": { + "version": "0.14.44", + "resolved": "https://registry.npmjs.org/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.14.44.tgz", + "integrity": "sha512-ri3Okw0aleYy7o5n9zlIq+FCtq3tcMlctN6X1H1ucILjBJuH8pan2trJPKWeb8ppntFvE28I9eEXhwkWh6wYKg==", + "dev": true, + "optional": true + }, + "esbuild-linux-ppc64le": { + "version": "0.14.44", + "resolved": "https://registry.npmjs.org/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.14.44.tgz", + "integrity": "sha512-96TqL/MvFRuIVXz+GtCIXzRQ43ZwEk4XTn0RWUNJduXXMDQ/V1iOV28U6x6Oe3NesK4xkoKSaK2+F3VHcU8ZrA==", + "dev": true, + "optional": true + }, + "esbuild-linux-riscv64": { + "version": "0.14.44", + "resolved": "https://registry.npmjs.org/esbuild-linux-riscv64/-/esbuild-linux-riscv64-0.14.44.tgz", + "integrity": "sha512-rrK9qEp2M8dhilsPn4T9gxUsAumkITc1kqYbpyNMr9EWo+J5ZBj04n3GYldULrcCw4ZCHAJ+qPjqr8b6kG2inA==", + "dev": true, + "optional": true + }, + "esbuild-linux-s390x": { + "version": "0.14.44", + "resolved": "https://registry.npmjs.org/esbuild-linux-s390x/-/esbuild-linux-s390x-0.14.44.tgz", + "integrity": "sha512-2YmTm9BrW5aUwBSe8wIEARd9EcnOQmkHp4+IVaO09Ez/C5T866x+ABzhG0bwx0b+QRo9q97CRMaQx2Ngb6/hfw==", + "dev": true, + "optional": true + }, + "esbuild-netbsd-64": { + "version": "0.14.44", + "resolved": "https://registry.npmjs.org/esbuild-netbsd-64/-/esbuild-netbsd-64-0.14.44.tgz", + "integrity": "sha512-zypdzPmZTCqYS30WHxbcvtC0E6e/ECvl4WueUdbdWhs2dfWJt5RtCBME664EpTznixR3lSN1MQ2NhwQF8MQryw==", + "dev": true, + "optional": true + }, + "esbuild-openbsd-64": { + "version": "0.14.44", + "resolved": "https://registry.npmjs.org/esbuild-openbsd-64/-/esbuild-openbsd-64-0.14.44.tgz", + "integrity": "sha512-8J43ab9ByYl7KteC03HGQjr2HY1ge7sN04lFnwMFWYk2NCn8IuaeeThvLeNjzOYhyT3I6K8puJP0uVXUu+D1xw==", + "dev": true, + "optional": true + }, + "esbuild-sunos-64": { + "version": "0.14.44", + "resolved": "https://registry.npmjs.org/esbuild-sunos-64/-/esbuild-sunos-64-0.14.44.tgz", + "integrity": "sha512-OH1/09CGUJwffA+HNM6mqPkSIyHVC3ZnURU/4CCIx7IqWUBn1Sh1HRLQC8/TWNgcs0/1u7ygnc2pgf/AHZJ/Ow==", + "dev": true, + "optional": true + }, + "esbuild-windows-32": { + "version": "0.14.44", + "resolved": "https://registry.npmjs.org/esbuild-windows-32/-/esbuild-windows-32-0.14.44.tgz", + "integrity": "sha512-mCAOL9/rRqwfOfxTu2sjq/eAIs7eAXGiU6sPBnowggI7QS953Iq6o3/uDu010LwfN7zr18c/lEj6/PTwwTB3AA==", + "dev": true, + "optional": true + }, + "esbuild-windows-64": { + "version": "0.14.44", + "resolved": "https://registry.npmjs.org/esbuild-windows-64/-/esbuild-windows-64-0.14.44.tgz", + "integrity": "sha512-AG6BH3+YG0s2Q/IfB1cm68FdyFnoE1P+GFbmgFO3tA4UIP8+BKsmKGGZ5I3+ZjcnzOwvT74bQRVrfnQow2KO5Q==", + "dev": true, + "optional": true + }, + "esbuild-windows-arm64": { + "version": "0.14.44", + "resolved": "https://registry.npmjs.org/esbuild-windows-arm64/-/esbuild-windows-arm64-0.14.44.tgz", + "integrity": "sha512-ygYPfYE5By4Sd6szsNr10B0RtWVNOSGmZABSaj4YQBLqh9b9i45VAjVWa8tyIy+UAbKF7WGwybi2wTbSVliO8A==", + "dev": true, + "optional": true + }, + "escalade": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", + "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", + "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": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==" + }, + "eslint": { + "version": "8.17.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.17.0.tgz", + "integrity": "sha512-gq0m0BTJfci60Fz4nczYxNAlED+sMcihltndR8t9t1evnU/azx53x3t2UHXC/uRjcbvRw/XctpaNygSTcQD+Iw==", + "dev": true, + "requires": { + "@eslint/eslintrc": "^1.3.0", + "@humanwhocodes/config-array": "^0.9.2", + "ajv": "^6.10.0", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.3.2", + "doctrine": "^3.0.0", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^7.1.1", + "eslint-utils": "^3.0.0", + "eslint-visitor-keys": "^3.3.0", + "espree": "^9.3.2", + "esquery": "^1.4.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "functional-red-black-tree": "^1.0.1", + "glob-parent": "^6.0.1", + "globals": "^13.15.0", + "ignore": "^5.2.0", + "import-fresh": "^3.0.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "js-yaml": "^4.1.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.1", + "regexpp": "^3.2.0", + "strip-ansi": "^6.0.1", + "strip-json-comments": "^3.1.0", + "text-table": "^0.2.0", + "v8-compile-cache": "^2.0.3" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true + }, + "eslint-scope": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.1.1.tgz", + "integrity": "sha512-QKQM/UXpIiHcLqJ5AOyIW7XZmzjkzQXYE54n1++wb0u9V/abW3l9uQnxX8Z5Xd18xyKIMTUAyQ0k1e8pz6LUrw==", + "dev": true, + "requires": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + } + }, + "globals": { + "version": "13.15.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.15.0.tgz", + "integrity": "sha512-bpzcOlgDhMG070Av0Vy5Owklpv1I6+j96GhUI7Rh7IzDCKLzboflLrrfqMu8NquDbiR4EOQk7XzJwqVJxicxog==", + "dev": true, + "requires": { + "type-fest": "^0.20.2" + } + }, + "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" + } + } + } + }, + "eslint-plugin-react": { + "version": "7.30.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.30.0.tgz", + "integrity": "sha512-RgwH7hjW48BleKsYyHK5vUAvxtE9SMPDKmcPRQgtRCYaZA0XQPt5FSkrU3nhz5ifzMZcA8opwmRJ2cmOO8tr5A==", + "dev": true, + "requires": { + "array-includes": "^3.1.5", + "array.prototype.flatmap": "^1.3.0", + "doctrine": "^2.1.0", + "estraverse": "^5.3.0", + "jsx-ast-utils": "^2.4.1 || ^3.0.0", + "minimatch": "^3.1.2", + "object.entries": "^1.1.5", + "object.fromentries": "^2.0.5", + "object.hasown": "^1.1.1", + "object.values": "^1.1.5", + "prop-types": "^15.8.1", + "resolve": "^2.0.0-next.3", + "semver": "^6.3.0", + "string.prototype.matchall": "^4.0.7" + }, + "dependencies": { + "doctrine": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", + "dev": true, + "requires": { + "esutils": "^2.0.2" + } + }, + "resolve": { + "version": "2.0.0-next.3", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.3.tgz", + "integrity": "sha512-W8LucSynKUIDu9ylraa7ueVZ7hc0uAgJBxVsQSKOXOyle8a93qXhcz+XAXZ8bIq2d6i4Ehddn6Evt+0/UwKk6Q==", + "dev": true, + "requires": { + "is-core-module": "^2.2.0", + "path-parse": "^1.0.6" + } + }, + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true + } + } + }, + "eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "dev": true, + "requires": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + }, + "dependencies": { + "estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true + } + } + }, + "eslint-utils": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-3.0.0.tgz", + "integrity": "sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA==", + "dev": true, + "requires": { + "eslint-visitor-keys": "^2.0.0" + }, + "dependencies": { + "eslint-visitor-keys": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz", + "integrity": "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==", + "dev": true + } + } + }, + "eslint-visitor-keys": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.3.0.tgz", + "integrity": "sha512-mQ+suqKJVyeuwGYHAdjMFqjCyfl8+Ldnxuyp3ldiMBFKkvytrXUZWaiPCEav8qDHKty44bD+qV1IP4T+w+xXRA==", + "dev": true + }, + "espree": { + "version": "9.3.2", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.3.2.tgz", + "integrity": "sha512-D211tC7ZwouTIuY5x9XnS0E9sWNChB7IYKX/Xp5eQj3nFXhqmiUDB9q27y76oFl8jTg3pXcQx/bpxMfs3CIZbA==", + "dev": true, + "requires": { + "acorn": "^8.7.1", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.3.0" + } + }, + "esquery": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.4.0.tgz", + "integrity": "sha512-cCDispWt5vHHtwMY2YrAQ4ibFkAL8RbH5YGBnZBc90MolvvfkkQcJro/aZiAQUlQ3qgrYS6D6v8Gc5G5CQsc9w==", + "dev": true, + "requires": { + "estraverse": "^5.1.0" + } + }, + "esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "requires": { + "estraverse": "^5.2.0" + } + }, + "estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "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 + }, + "esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true + }, + "fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" + }, + "fast-glob": { + "version": "3.2.11", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.11.tgz", + "integrity": "sha512-xrO3+1bxSo3ZVHAnqzyuewYT6aMFHRAd4Kcs92MAonjwQZLsK9d0SF1IyQ3k5PoirxTW0Oe/RqFgMQ6TcNE5Ew==", + "dev": true, + "requires": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + }, + "dependencies": { + "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" + } + } + } + }, + "fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true + }, + "fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true + }, + "fastq": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.13.0.tgz", + "integrity": "sha512-YpkpUnK8od0o1hmeSc7UUs/eB/vIPWJYjKck2QKIzAf71Vm1AAQ3EbuZB3g2JIy+pg+ERD0vqI79KyZiB2e2Nw==", + "dev": true, + "requires": { + "reusify": "^1.0.4" + } + }, + "file-entry-cache": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "dev": true, + "requires": { + "flat-cache": "^3.0.4" + } + }, + "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" + } + }, + "find-root": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz", + "integrity": "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==" + }, + "find-up": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", + "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", + "requires": { + "locate-path": "^3.0.0" + } + }, + "flat-cache": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.0.4.tgz", + "integrity": "sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg==", + "dev": true, + "requires": { + "flatted": "^3.1.0", + "rimraf": "^3.0.2" + } + }, + "flatted": { + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.5.tgz", + "integrity": "sha512-WIWGi2L3DyTUvUrwRKgGi9TwxQMUEqPOPQBVi71R96jZXJdFskXEmf54BoZaS1kknGODoIGASGEzBUYdyMCBJg==", + "dev": true + }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==" + }, + "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==" + }, + "function.prototype.name": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.5.tgz", + "integrity": "sha512-uN7m/BzVKQnCUF/iW8jYea67v++2u7m5UgENbHRtdDVclOUP+FMPlCNdmk0h/ysGyo2tavMJEDqJAkJdRa1vMA==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3", + "es-abstract": "^1.19.0", + "functions-have-names": "^1.2.2" + } + }, + "functional-red-black-tree": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz", + "integrity": "sha512-dsKNQNdj6xA3T+QlADDA7mOSlX0qiMINjn0cgr+eGHGsbSHzTabcIogz2+p/iqP1Xs6EP/sS2SbqH+brGTbq0g==", + "dev": true + }, + "functions-have-names": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", + "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", + "dev": true + }, + "gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true + }, + "get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==" + }, + "get-intrinsic": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.2.tgz", + "integrity": "sha512-Jfm3OyCxHh9DJyc28qGk+JmfkpO41A4XkneDSujN9MDXrm4oDKdHvndhZ2dN94+ERNfkYJWDclW6k2L/ZGHjXA==", + "dev": true, + "requires": { + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.3" + } + }, + "get-symbol-description": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.0.tgz", + "integrity": "sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.1.1" + } + }, + "glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "requires": { + "is-glob": "^4.0.3" + } + }, + "globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "dev": true + }, + "globby": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "dev": true, + "requires": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^3.0.0" + } + }, + "goober": { + "version": "2.1.13", + "resolved": "https://registry.npmjs.org/goober/-/goober-2.1.13.tgz", + "integrity": "sha512-jFj3BQeleOoy7t93E9rZ2de+ScC4lQICLwiAQmKMg9F6roKGaLSHoCDYKkWlSafg138jejvq/mTdvmnwDQgqoQ==", + "requires": {} + }, + "has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "requires": { + "function-bind": "^1.1.1" + } + }, + "has-bigints": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz", + "integrity": "sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==", + "dev": true + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==" + }, + "has-property-descriptors": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.0.tgz", + "integrity": "sha512-62DVLZGoiEBDHQyqG4w9xCuZ7eJEwNmJRWw2VY84Oedb7WFcA27fiEVe8oUQx9hAUJ4ekurquucTGwsyO1XGdQ==", + "dev": true, + "requires": { + "get-intrinsic": "^1.1.1" + } + }, + "has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "dev": true + }, + "has-tostringtag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.0.tgz", + "integrity": "sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==", + "dev": true, + "requires": { + "has-symbols": "^1.0.2" + } + }, + "highlight.js": { + "version": "11.5.1", + "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-11.5.1.tgz", + "integrity": "sha512-LKzHqnxr4CrD2YsNoIf/o5nJ09j4yi/GcH5BnYz9UnVpZdS4ucMgvP61TDty5xJcFGRjnH4DpujkS9bHT3hq0Q==" + }, + "hoist-non-react-statics": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", + "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", + "requires": { + "react-is": "^16.7.0" + }, + "dependencies": { + "react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" + } + } + }, + "ignore": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.0.tgz", + "integrity": "sha512-CmxgYGiEPCLhfLnpPp1MoRmifwEIOgjcHXxOBjv7mY96c+eWScsOP9c112ZyLdWHi0FxHjI+4uVhKYp/gcdRmQ==", + "dev": true + }, + "immutable": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.1.0.tgz", + "integrity": "sha512-oNkuqVTA8jqG1Q6c+UglTOD1xhC1BtjKI7XkCXRkZHrN5m18/XsnUp8Q89GkQO/z+0WjonSvl0FLhDYftp46nQ==", + "dev": true + }, + "import-fresh": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", + "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "requires": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + } + }, + "imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "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==" + }, + "internal-slot": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.3.tgz", + "integrity": "sha512-O0DB1JC/sPyZl7cIo78n5dR7eUSwwpYPiXRhTzNxZVAMUuB8vlnRFyLxdrVToks6XPLVnFfbzaVd5WLjhgg+vA==", + "dev": true, + "requires": { + "get-intrinsic": "^1.1.0", + "has": "^1.0.3", + "side-channel": "^1.0.4" + } + }, + "is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==" + }, + "is-bigint": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.4.tgz", + "integrity": "sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==", + "dev": true, + "requires": { + "has-bigints": "^1.0.1" + } + }, + "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-boolean-object": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz", + "integrity": "sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + } + }, + "is-callable": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.4.tgz", + "integrity": "sha512-nsuwtxZfMX67Oryl9LCQ+upnC0Z0BgpwntpS89m1H/TLF0zNfzfLMV/9Wa/6MZsj0acpEjAO0KF1xT6ZdLl95w==", + "dev": true + }, + "is-core-module": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.9.0.tgz", + "integrity": "sha512-+5FPy5PnwmO3lvfMb0AsoPaBG+5KHUI0wYFXOtYPnVVVspTFUuMZNfNaNVRt3FZadstu2c8x23vykRW/NBoU6A==", + "requires": { + "has": "^1.0.3" + } + }, + "is-date-object": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz", + "integrity": "sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==", + "dev": true, + "requires": { + "has-tostringtag": "^1.0.0" + } + }, + "is-docker": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", + "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", + "dev": true + }, + "is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "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-negative-zero": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.2.tgz", + "integrity": "sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA==", + "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-number-object": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.7.tgz", + "integrity": "sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==", + "dev": true, + "requires": { + "has-tostringtag": "^1.0.0" + } + }, + "is-regex": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", + "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + } + }, + "is-shared-array-buffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.2.tgz", + "integrity": "sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA==", + "dev": true, + "requires": { + "call-bind": "^1.0.2" + } + }, + "is-string": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz", + "integrity": "sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==", + "dev": true, + "requires": { + "has-tostringtag": "^1.0.0" + } + }, + "is-symbol": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.4.tgz", + "integrity": "sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==", + "dev": true, + "requires": { + "has-symbols": "^1.0.2" + } + }, + "is-weakref": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.2.tgz", + "integrity": "sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==", + "dev": true, + "requires": { + "call-bind": "^1.0.2" + } + }, + "is-wsl": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", + "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", + "dev": true, + "requires": { + "is-docker": "^2.0.0" + } + }, + "isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true + }, + "jest-diff": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-26.6.2.tgz", + "integrity": "sha512-6m+9Z3Gv9wN0WFVasqjCL/06+EFCMTqDEUl/b87HYK2rAPTyfz4ZIuSlPhY51PIQRWx5TaxeF1qmXKe9gfN3sA==", + "dev": true, + "requires": { + "chalk": "^4.0.0", + "diff-sequences": "^26.6.2", + "jest-get-type": "^26.3.0", + "pretty-format": "^26.6.2" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "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" + } + } + } + }, + "jest-get-type": { + "version": "26.3.0", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-26.3.0.tgz", + "integrity": "sha512-TpfaviN1R2pQWkIihlfEanwOXK0zcxrKEE4MlU6Tn7keoXdN6/3gK/xl0yEh8DOunn5pOVGKf8hB4R9gVh04ig==", + "dev": true + }, + "js-sha3": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/js-sha3/-/js-sha3-0.8.0.tgz", + "integrity": "sha512-gF1cRrHhIzNfToc802P800N8PpXS+evLLXfsVpowqmAFR9uwbi89WvXg2QspOmXL8QL86J4T1EpFu+yUkwJY3Q==" + }, + "js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" + }, + "js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "requires": { + "argparse": "^2.0.1" + } + }, + "jsesc": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", + "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", + "dev": true + }, + "json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==" + }, + "json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true + }, + "json5": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.1.tgz", + "integrity": "sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA==", + "dev": true + }, + "jsx-ast-utils": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.0.tgz", + "integrity": "sha512-XzO9luP6L0xkxwhIJMTJQpZo/eeN60K08jHdexfD569AGxeNug6UketeHXEhROoM8aR7EcUoOQmIhcJQjcuq8Q==", + "dev": true, + "requires": { + "array-includes": "^3.1.4", + "object.assign": "^4.1.2" + } + }, + "levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "requires": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + } + }, + "lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==" + }, + "locate-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", + "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", + "requires": { + "p-locate": "^3.0.0", + "path-exists": "^3.0.0" + } + }, + "lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + }, + "lodash-es": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz", + "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==" + }, + "lodash.debounce": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", + "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==" + }, + "lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true + }, + "loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "requires": { + "js-tokens": "^3.0.0 || ^4.0.0" + } + }, + "lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "requires": { + "yallist": "^4.0.0" + } + }, + "match-sorter": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/match-sorter/-/match-sorter-6.3.1.tgz", + "integrity": "sha512-mxybbo3pPNuA+ZuCUhm5bwNkXrJTbsk5VWbR5wiwz/GC6LIiegBGn2w3O08UG/jdbYLinw51fSQ5xNU1U3MgBw==", + "requires": { + "@babel/runtime": "^7.12.5", + "remove-accents": "0.4.2" + } + }, + "memoize-one": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-5.2.1.tgz", + "integrity": "sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==" + }, + "merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true + }, + "micromatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", + "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "dev": true, + "requires": { + "braces": "^3.0.2", + "picomatch": "^2.3.1" + } + }, + "microseconds": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/microseconds/-/microseconds-0.2.0.tgz", + "integrity": "sha512-n7DHHMjR1avBbSpsTBj6fmMGh2AGrifVV4e+WYc3Q9lO+xnSZ3NyhcBND3vzzatt05LFhoKFRxrIyklmLlUtyA==" + }, + "minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "moment": { + "version": "2.29.4", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.4.tgz", + "integrity": "sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w==" + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "nano-time": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/nano-time/-/nano-time-1.0.0.tgz", + "integrity": "sha512-flnngywOoQ0lLQOTRNexn2gGSNuM9bKj9RZAWSzhQ+UJYaAFG9bac4DW9VHjUAzrOaIcajHybCTHe/bkvozQqA==", + "requires": { + "big-integer": "^1.6.16" + } + }, + "nanoclone": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/nanoclone/-/nanoclone-0.2.1.tgz", + "integrity": "sha512-wynEP02LmIbLpcYw8uBKpcfF6dmg2vcpKqxeH5UcoKEYdExslsdUA4ugFauuaeYdTB76ez6gJW8XAZ6CgkXYxA==" + }, + "nanoid": { + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.4.tgz", + "integrity": "sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw==", + "dev": true + }, + "natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true + }, + "node-releases": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.5.tgz", + "integrity": "sha512-U9h1NLROZTq9uE1SNffn6WuPDg8icmi3ns4rEl/oTfIle4iLjTliCzgTsbaIFMq/Xn078/lfY/BL0GWZ+psK4Q==", + "dev": true + }, + "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 + }, + "object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==" + }, + "object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==" + }, + "object-inspect": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.2.tgz", + "integrity": "sha512-z+cPxW0QGUp0mcqcsgQyLVRDoXFQbXOwBaqyF7VIgI4TWNQsDHrBpUQslRmIfAoYWdYzs6UlKJtB2XJpTaNSpQ==", + "dev": true + }, + "object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true + }, + "object.assign": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.2.tgz", + "integrity": "sha512-ixT2L5THXsApyiUPYKmW+2EHpXXe5Ii3M+f4e+aJFAHao5amFRW6J0OO6c/LU8Be47utCx2GL89hxGB6XSmKuQ==", + "dev": true, + "requires": { + "call-bind": "^1.0.0", + "define-properties": "^1.1.3", + "has-symbols": "^1.0.1", + "object-keys": "^1.1.1" + } + }, + "object.entries": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.5.tgz", + "integrity": "sha512-TyxmjUoZggd4OrrU1W66FMDG6CuqJxsFvymeyXI51+vQLN67zYfZseptRge703kKQdo4uccgAKebXFcRCzk4+g==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3", + "es-abstract": "^1.19.1" + } + }, + "object.fromentries": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.5.tgz", + "integrity": "sha512-CAyG5mWQRRiBU57Re4FKoTBjXfDoNwdFVH2Y1tS9PqCsfUTymAohOkEMSG3aRNKmv4lV3O7p1et7c187q6bynw==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3", + "es-abstract": "^1.19.1" + } + }, + "object.hasown": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object.hasown/-/object.hasown-1.1.1.tgz", + "integrity": "sha512-LYLe4tivNQzq4JdaWW6WO3HMZZJWzkkH8fnI6EebWl0VZth2wL2Lovm74ep2/gZzlaTdV62JZHEqHQ2yVn8Q/A==", + "dev": true, + "requires": { + "define-properties": "^1.1.4", + "es-abstract": "^1.19.5" + } + }, + "object.values": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.1.5.tgz", + "integrity": "sha512-QUZRW0ilQ3PnPpbNtgdNV1PDbEqLIiSFB3l+EnGtBQ/8SUTLj1PZwtQHABZtLgwpJZTSZhuGLOGk57Drx2IvYg==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3", + "es-abstract": "^1.19.1" + } + }, + "oblivious-set": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/oblivious-set/-/oblivious-set-1.0.0.tgz", + "integrity": "sha512-z+pI07qxo4c2CulUHCDf9lcqDlMSo72N/4rLUpRXf6fu+q8vjt8y0xS+Tlf8NTJDdTXHbdeO1n3MlbctwEoXZw==" + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "requires": { + "wrappy": "1" + } + }, + "open": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/open/-/open-8.4.0.tgz", + "integrity": "sha512-XgFPPM+B28FtCCgSb9I+s9szOC1vZRSwgWsRUA5ylIxRTgKozqjOCrVOqGsYABPYK5qnfqClxZTFBa8PKt2v6Q==", + "dev": true, + "requires": { + "define-lazy-prop": "^2.0.0", + "is-docker": "^2.1.1", + "is-wsl": "^2.2.0" + } + }, + "optionator": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz", + "integrity": "sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==", + "dev": true, + "requires": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.3" + } + }, + "p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "requires": { + "p-try": "^2.0.0" + } + }, + "p-locate": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", + "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", + "requires": { + "p-limit": "^2.0.0" + } + }, + "p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==" + }, + "packet-reader": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/packet-reader/-/packet-reader-1.0.0.tgz", + "integrity": "sha512-HAKu/fG3HpHFO0AA8WE8q2g+gBJaZ9MG7fcKk+IJPLTGAD6Psw4443l+9DGRbOIh3/aXr7Phy0TjilYivJo5XQ==", + "dev": true + }, + "parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "requires": { + "callsites": "^3.0.0" + } + }, + "parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "requires": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + } + }, + "path-exists": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", + "integrity": "sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==" + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==" + }, + "path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "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==" + }, + "path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==" + }, + "pg": { + "version": "8.11.3", + "resolved": "https://registry.npmjs.org/pg/-/pg-8.11.3.tgz", + "integrity": "sha512-+9iuvG8QfaaUrrph+kpF24cXkH1YOOUeArRNYIxq1viYHZagBxrTno7cecY1Fa44tJeZvaoG+Djpkc3JwehN5g==", + "dev": true, + "requires": { + "buffer-writer": "2.0.0", + "packet-reader": "1.0.0", + "pg-cloudflare": "^1.1.1", + "pg-connection-string": "^2.6.2", + "pg-pool": "^3.6.1", + "pg-protocol": "^1.6.0", + "pg-types": "^2.1.0", + "pgpass": "1.x" + } + }, + "pg-cloudflare": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.1.1.tgz", + "integrity": "sha512-xWPagP/4B6BgFO+EKz3JONXv3YDgvkbVrGw2mTo3D6tVDQRh1e7cqVGvyR3BE+eQgAvx1XhW/iEASj4/jCWl3Q==", + "dev": true, + "optional": true + }, + "pg-connection-string": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.6.2.tgz", + "integrity": "sha512-ch6OwaeaPYcova4kKZ15sbJ2hKb/VP48ZD2gE7i1J+L4MspCtBMAx8nMgz7bksc7IojCIIWuEhHibSMFH8m8oA==", + "dev": true + }, + "pg-int8": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz", + "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==", + "dev": true + }, + "pg-pool": { + "version": "3.6.1", + "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.6.1.tgz", + "integrity": "sha512-jizsIzhkIitxCGfPRzJn1ZdcosIt3pz9Sh3V01fm1vZnbnCMgmGl5wvGGdNN2EL9Rmb0EcFoCkixH4Pu+sP9Og==", + "dev": true, + "requires": {} + }, + "pg-protocol": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.6.0.tgz", + "integrity": "sha512-M+PDm637OY5WM307051+bsDia5Xej6d9IR4GwJse1qA1DIhiKlksvrneZOYQq42OM+spubpcNYEo2FcKQrDk+Q==", + "dev": true + }, + "pg-types": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz", + "integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==", + "dev": true, + "requires": { + "pg-int8": "1.0.1", + "postgres-array": "~2.0.0", + "postgres-bytea": "~1.0.0", + "postgres-date": "~1.0.4", + "postgres-interval": "^1.1.0" + } + }, + "pgpass": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz", + "integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==", + "dev": true, + "requires": { + "split2": "^4.1.0" + } + }, + "picocolors": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", + "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", + "dev": true + }, + "picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true + }, + "playwright": { + "version": "1.41.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.41.1.tgz", + "integrity": "sha512-gdZAWG97oUnbBdRL3GuBvX3nDDmUOuqzV/D24dytqlKt+eI5KbwusluZRGljx1YoJKZ2NRPaeWiFTeGZO7SosQ==", + "dev": true, + "requires": { + "fsevents": "2.3.2", + "playwright-core": "1.41.1" + } + }, + "playwright-core": { + "version": "1.41.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.41.1.tgz", + "integrity": "sha512-/KPO5DzXSMlxSX77wy+HihKGOunh3hqndhqeo/nMxfigiKzogn8kfL0ZBDu0L1RKgan5XHCPmn6zXd2NUJgjhg==", + "dev": true + }, + "postcss": { + "version": "8.4.14", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.14.tgz", + "integrity": "sha512-E398TUmfAYFPBSdzgeieK2Y1+1cpdxJx8yXbK/m57nRhKSmk1GB2tO4lbLBtlkfPQTDKfe4Xqv1ASWPpayPEig==", + "dev": true, + "requires": { + "nanoid": "^3.3.4", + "picocolors": "^1.0.0", + "source-map-js": "^1.0.2" + } + }, + "postgres-array": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz", + "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==", + "dev": true + }, + "postgres-bytea": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.0.tgz", + "integrity": "sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w==", + "dev": true + }, + "postgres-date": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz", + "integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==", + "dev": true + }, + "postgres-interval": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz", + "integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==", + "dev": true, + "requires": { + "xtend": "^4.0.0" + } + }, + "prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true + }, + "prettier": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.6.2.tgz", + "integrity": "sha512-PkUpF+qoXTqhOeWL9fu7As8LXsIUZ1WYaJiY/a7McAQzxjk82OF0tibkFXVCDImZtWxbvojFjerkiLb0/q8mew==", + "dev": true + }, + "pretty-format": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-26.6.2.tgz", + "integrity": "sha512-7AeGuCYNGmycyQbCqd/3PWH4eOoX/OiCa0uphp57NVTeAGdJGaAliecxwBDHYQCIvrW7aDBZCYeNTP/WX69mkg==", + "dev": true, + "requires": { + "@jest/types": "^26.6.2", + "ansi-regex": "^5.0.0", + "ansi-styles": "^4.0.0", + "react-is": "^17.0.1" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + } + } + }, + "process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", + "dev": true + }, + "prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "requires": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + }, + "dependencies": { + "react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" + } + } + }, + "property-expr": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/property-expr/-/property-expr-2.0.5.tgz", + "integrity": "sha512-IJUkICM5dP5znhCckHSv30Q4b5/JA5enCtkRHYaOVOAocnH/1BQEYTC5NMfT3AVl/iXKdr3aqQbQn9DxyWknwA==" + }, + "punycode": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", + "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", + "dev": true + }, + "queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true + }, + "react": { + "version": "18.1.0", + "resolved": "https://registry.npmjs.org/react/-/react-18.1.0.tgz", + "integrity": "sha512-4oL8ivCz5ZEPyclFQXaNksK3adutVS8l2xzZU0cqEFrE9Sb7fC0EFK5uEk74wIreL1DERyjvsU915j1pcT2uEQ==", + "requires": { + "loose-envify": "^1.1.0" + } + }, + "react-dom": { + "version": "18.1.0", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.1.0.tgz", + "integrity": "sha512-fU1Txz7Budmvamp7bshe4Zi32d0ll7ect+ccxNu9FlObT605GOEB8BfO4tmRJ39R5Zj831VCpvQ05QPBW5yb+w==", + "requires": { + "loose-envify": "^1.1.0", + "scheduler": "^0.22.0" + } + }, + "react-hook-form": { + "version": "7.48.2", + "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.48.2.tgz", + "integrity": "sha512-H0T2InFQb1hX7qKtDIZmvpU1Xfn/bdahWBN1fH19gSe4bBEqTfmlr7H3XWTaVtiK4/tpPaI1F3355GPMZYge+A==", + "requires": {} + }, + "react-hot-toast": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/react-hot-toast/-/react-hot-toast-2.4.1.tgz", + "integrity": "sha512-j8z+cQbWIM5LY37pR6uZR6D4LfseplqnuAO4co4u8917hBUvXlEqyP1ZzqVLcqoyUesZZv/ImreoCeHVDpE5pQ==", + "requires": { + "goober": "^2.1.10" + } + }, + "react-is": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", + "dev": true + }, + "react-query": { + "version": "3.39.1", + "resolved": "https://registry.npmjs.org/react-query/-/react-query-3.39.1.tgz", + "integrity": "sha512-qYKT1bavdDiQZbngWZyPotlBVzcBjDYEJg5RQLBa++5Ix5jjfbEYJmHSZRZD+USVHUSvl/ey9Hu+QfF1QAK80A==", + "requires": { + "@babel/runtime": "^7.5.5", + "broadcast-channel": "^3.4.1", + "match-sorter": "^6.0.2" + } + }, + "react-refresh": { + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.13.0.tgz", + "integrity": "sha512-XP8A9BT0CpRBD+NYLLeIhld/RqG9+gktUjW1FkE+Vm7OCinbG1SshcK5tb9ls4kzvjZr9mOQc7HYgBngEyPAXg==", + "dev": true + }, + "react-router": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.4.0.tgz", + "integrity": "sha512-B+5bEXFlgR1XUdHYR6P94g299SjrfCBMmEDJNcFbpAyRH1j1748yt9NdDhW3++nw1lk3zQJ6aOO66zUx3KlTZg==", + "requires": { + "@remix-run/router": "1.0.0" + } + }, + "react-router-dom": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.4.0.tgz", + "integrity": "sha512-4Aw1xmXKeleYYQ3x0Lcl2undHR6yMjXZjd9DKZd53SGOYqirrUThyUb0wwAX5VZAyvSuzjNJmZlJ3rR9+/vzqg==", + "requires": { + "react-router": "6.4.0" + } + }, + "react-select": { + "version": "5.7.7", + "resolved": "https://registry.npmjs.org/react-select/-/react-select-5.7.7.tgz", + "integrity": "sha512-HhashZZJDRlfF/AKj0a0Lnfs3sRdw/46VJIRd8IbB9/Ovr74+ZIwkAdSBjSPXsFMG+u72c5xShqwLSKIJllzqw==", + "requires": { + "@babel/runtime": "^7.12.0", + "@emotion/cache": "^11.4.0", + "@emotion/react": "^11.8.1", + "@floating-ui/dom": "^1.0.1", + "@types/react-transition-group": "^4.4.0", + "memoize-one": "^6.0.0", + "prop-types": "^15.6.0", + "react-transition-group": "^4.3.0", + "use-isomorphic-layout-effect": "^1.1.2" + }, + "dependencies": { + "memoize-one": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-6.0.0.tgz", + "integrity": "sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==" + } + } + }, + "react-tooltip": { + "version": "5.14.0", + "resolved": "https://registry.npmjs.org/react-tooltip/-/react-tooltip-5.14.0.tgz", + "integrity": "sha512-fvHgZKSdDC/Zv7G7IL6kPJi99Fqty6zkQsYQZGSkhMmmDQhVnsBLAmY8s14Nh8bshgPmR5EvbjUDZg0yMsp1hg==", + "requires": { + "@floating-ui/dom": "^1.0.0", + "classnames": "^2.3.0" + } + }, + "react-transition-group": { + "version": "4.4.5", + "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz", + "integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==", + "requires": { + "@babel/runtime": "^7.5.5", + "dom-helpers": "^5.0.1", + "loose-envify": "^1.4.0", + "prop-types": "^15.6.2" + } + }, + "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" + } + }, + "regenerator-runtime": { + "version": "0.13.11", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", + "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==" + }, + "regexp.prototype.flags": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.4.3.tgz", + "integrity": "sha512-fjggEOO3slI6Wvgjwflkc4NFRCTZAu5CnNfBd5qOMYhWdn67nJBBu34/TkD++eeFmd8C9r9jfXJ27+nSiRkSUA==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3", + "functions-have-names": "^1.2.2" + } + }, + "regexpp": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.2.0.tgz", + "integrity": "sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==", + "dev": true + }, + "remove-accents": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/remove-accents/-/remove-accents-0.4.2.tgz", + "integrity": "sha512-7pXIJqJOq5tFgG1A2Zxti3Ht8jJF337m4sowbuHsW30ZnkQFnDzy9qBNhgzX8ZLW4+UBcXiiR7SwR6pokHsxiA==" + }, + "require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==" + }, + "require-main-filename": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", + "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==" + }, + "resolve": { + "version": "1.22.0", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.0.tgz", + "integrity": "sha512-Hhtrw0nLeSrFQ7phPp4OOcVjLPIeMnRlr5mcnVuMe7M/7eBn98A3hmFRLoFo3DLZkivSYwhRUJTyPyWAk56WLw==", + "requires": { + "is-core-module": "^2.8.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + } + }, + "resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==" + }, + "reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "dev": true + }, + "rifm": { + "version": "0.12.1", + "resolved": "https://registry.npmjs.org/rifm/-/rifm-0.12.1.tgz", + "integrity": "sha512-OGA1Bitg/dSJtI/c4dh90svzaUPt228kzFsUkJbtA2c964IqEAwWXeL9ZJi86xWv3j5SMqRvGULl7bA6cK0Bvg==", + "requires": {} + }, + "rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "requires": { + "glob": "^7.1.3" + } + }, + "rollup": { + "version": "2.75.6", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.75.6.tgz", + "integrity": "sha512-OEf0TgpC9vU6WGROJIk1JA3LR5vk/yvqlzxqdrE2CzzXnqKXNzbAwlWUXis8RS3ZPe7LAq+YUxsRa0l3r27MLA==", + "dev": true, + "requires": { + "fsevents": "~2.3.2" + } + }, + "rollup-plugin-analyzer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/rollup-plugin-analyzer/-/rollup-plugin-analyzer-4.0.0.tgz", + "integrity": "sha512-LL9GEt3bkXp6Wa19SNR5MWcvHNMvuTFYg+eYBZN2OIFhSWN+pEJUQXEKu5BsOeABob3x9PDaLKW7w5iOJnsESQ==", + "dev": true + }, + "rollup-plugin-visualizer": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/rollup-plugin-visualizer/-/rollup-plugin-visualizer-5.6.0.tgz", + "integrity": "sha512-CKcc8GTUZjC+LsMytU8ocRr/cGZIfMR7+mdy4YnlyetlmIl/dM8BMnOEpD4JPIGt+ZVW7Db9ZtSsbgyeBH3uTA==", + "dev": true, + "requires": { + "nanoid": "^3.1.32", + "open": "^8.4.0", + "source-map": "^0.7.3", + "yargs": "^17.3.1" + } + }, + "run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "requires": { + "queue-microtask": "^1.2.2" + } + }, + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "sass": { + "version": "1.55.0", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.55.0.tgz", + "integrity": "sha512-Pk+PMy7OGLs9WaxZGJMn7S96dvlyVBwwtToX895WmCpAOr5YiJYEUJfiJidMuKb613z2xNWcXCHEuOvjZbqC6A==", + "dev": true, + "requires": { + "chokidar": ">=3.0.0 <4.0.0", + "immutable": "^4.0.0", + "source-map-js": ">=0.6.2 <2.0.0" + } + }, + "scheduler": { + "version": "0.22.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.22.0.tgz", + "integrity": "sha512-6QAm1BgQI88NPYymgGQLCZgvep4FyePDWFpXVK+zNSUgHwlqpJy8VEh8Et0KxTACS4VWwMousBElAZOH9nkkoQ==", + "requires": { + "loose-envify": "^1.1.0" + } + }, + "semver": { + "version": "7.3.7", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.7.tgz", + "integrity": "sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g==", + "dev": true, + "requires": { + "lru-cache": "^6.0.0" + } + }, + "set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==" + }, + "shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "requires": { + "shebang-regex": "^3.0.0" + } + }, + "shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true + }, + "showdown": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/showdown/-/showdown-1.9.1.tgz", + "integrity": "sha512-9cGuS382HcvExtf5AHk7Cb4pAeQQ+h0eTr33V1mu+crYWV4KvWAw6el92bDrqGEk5d46Ai/fhbEUwqJ/mTCNEA==", + "requires": { + "yargs": "^14.2" + }, + "dependencies": { + "ansi-regex": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.1.tgz", + "integrity": "sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g==" + }, + "cliui": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-5.0.0.tgz", + "integrity": "sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA==", + "requires": { + "string-width": "^3.1.0", + "strip-ansi": "^5.2.0", + "wrap-ansi": "^5.1.0" + } + }, + "emoji-regex": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", + "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==" + }, + "is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha512-VHskAKYM8RfSFXwee5t5cbN5PZeq1Wrh6qd5bkyiXIf6UQcN6w/A0eXM9r6t8d+GYOh+o6ZhiEnb88LN/Y8m2w==" + }, + "string-width": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", + "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", + "requires": { + "emoji-regex": "^7.0.1", + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^5.1.0" + } + }, + "strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "requires": { + "ansi-regex": "^4.1.0" + } + }, + "wrap-ansi": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-5.1.0.tgz", + "integrity": "sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q==", + "requires": { + "ansi-styles": "^3.2.0", + "string-width": "^3.0.0", + "strip-ansi": "^5.0.0" + } + }, + "y18n": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", + "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==" + }, + "yargs": { + "version": "14.2.3", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-14.2.3.tgz", + "integrity": "sha512-ZbotRWhF+lkjijC/VhmOT9wSgyBQ7+zr13+YLkhfsSiTriYsMzkTUFP18pFhWwBeMa5gUc1MzbhrO6/VB7c9Xg==", + "requires": { + "cliui": "^5.0.0", + "decamelize": "^1.2.0", + "find-up": "^3.0.0", + "get-caller-file": "^2.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^3.0.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^15.0.1" + } + }, + "yargs-parser": { + "version": "15.0.3", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-15.0.3.tgz", + "integrity": "sha512-/MVEVjTXy/cGAjdtQf8dW3V9b97bPN7rNn8ETj6BmAQL7ibC7O1Q9SPJbGjgh3SlwoBNXMzj/ZGIj8mBgl12YA==", + "requires": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + } + } + } + }, + "side-channel": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", + "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", + "dev": true, + "requires": { + "call-bind": "^1.0.0", + "get-intrinsic": "^1.0.2", + "object-inspect": "^1.9.0" + } + }, + "slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true + }, + "source-map": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz", + "integrity": "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==", + "dev": true + }, + "source-map-js": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", + "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==", + "dev": true + }, + "split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "dev": true + }, + "string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "requires": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + } + }, + "string.prototype.matchall": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.7.tgz", + "integrity": "sha512-f48okCX7JiwVi1NXCVWcFnZgADDC/n2vePlQ/KUCNqCikLLilQvwjMO8+BHVKvgzH0JB0J9LEPgxOGT02RoETg==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3", + "es-abstract": "^1.19.1", + "get-intrinsic": "^1.1.1", + "has-symbols": "^1.0.3", + "internal-slot": "^1.0.3", + "regexp.prototype.flags": "^1.4.1", + "side-channel": "^1.0.4" + } + }, + "string.prototype.trimend": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.5.tgz", + "integrity": "sha512-I7RGvmjV4pJ7O3kdf+LXFpVfdNOxtCW/2C8f6jNiW4+PQchwxkCDzlk1/7p+Wl4bqFIZeF47qAHXLuHHWKAxog==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "es-abstract": "^1.19.5" + } + }, + "string.prototype.trimstart": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.5.tgz", + "integrity": "sha512-THx16TJCGlsN0o6dl2o6ncWUsdgnLRSA23rRE5pyGBw/mLr3Ej/R2LaqCtgP8VNMGZsvMWnf9ooZPyY2bHvUFg==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "es-abstract": "^1.19.5" + } + }, + "strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "requires": { + "ansi-regex": "^5.0.1" + } + }, + "strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true + }, + "style-mod": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.0.0.tgz", + "integrity": "sha512-OPhtyEjyyN9x3nhPsu76f52yUGXiZcgvsrFVtvTkyGRQJ0XK+GPc6ov1z+lRpbeabka+MYEQxOYRnt5nF30aMw==" + }, + "stylis": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.2.0.tgz", + "integrity": "sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==" + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "requires": { + "has-flag": "^3.0.0" + } + }, + "supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==" + }, + "text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", + "dev": true + }, + "to-fast-properties": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", + "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==" + }, + "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" + } + }, + "toggle-selection": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/toggle-selection/-/toggle-selection-1.0.6.tgz", + "integrity": "sha512-BiZS+C1OS8g/q2RRbJmy59xpyghNBqrr6k5L/uKBGRsTfxmu3ffiRnd8mlGPUVayg8pvfi5urfnu8TU7DVOkLQ==" + }, + "toposort": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/toposort/-/toposort-2.0.2.tgz", + "integrity": "sha512-0a5EOkAUp8D4moMi2W8ZF8jcga7BgZd91O/yabJCFY8az+XSzeGyTKs0Aoo897iV1Nj6guFq8orWDS96z91oGg==" + }, + "tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true + }, + "tsutils": { + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz", + "integrity": "sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==", + "dev": true, + "requires": { + "tslib": "^1.8.1" + } + }, + "type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "requires": { + "prelude-ls": "^1.2.1" + } + }, + "type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true + }, + "typescript": { + "version": "4.7.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.7.3.tgz", + "integrity": "sha512-WOkT3XYvrpXx4vMMqlD+8R8R37fZkjyLGlxavMc4iB8lrl8L0DeTcHbYgw/v0N/z9wAFsgBhcsF0ruoySS22mA==", + "dev": true + }, + "unbox-primitive": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz", + "integrity": "sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "has-bigints": "^1.0.2", + "has-symbols": "^1.0.3", + "which-boxed-primitive": "^1.0.2" + } + }, + "unload": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/unload/-/unload-2.2.0.tgz", + "integrity": "sha512-B60uB5TNBLtN6/LsgAf3udH9saB5p7gqJwcFfbOEZ8BcBHnGwCf6G/TGiEqkRAxX7zAFIUtzdrXQSdL3Q/wqNA==", + "requires": { + "@babel/runtime": "^7.6.2", + "detect-node": "^2.0.4" + } + }, + "uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "requires": { + "punycode": "^2.1.0" + } + }, + "use-isomorphic-layout-effect": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/use-isomorphic-layout-effect/-/use-isomorphic-layout-effect-1.1.2.tgz", + "integrity": "sha512-49L8yCO3iGT/ZF9QttjwLF/ZD9Iwto5LnH5LmEdk/6cFmXddqi2ulF0edxTwjj+7mqvpVVGQWvbXZdn32wRSHA==", + "requires": {} + }, + "use-sync-external-store": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz", + "integrity": "sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==", + "requires": {} + }, + "uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==" + }, + "v8-compile-cache": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz", + "integrity": "sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA==", + "dev": true + }, + "vite": { + "version": "2.9.12", + "resolved": "https://registry.npmjs.org/vite/-/vite-2.9.12.tgz", + "integrity": "sha512-suxC36dQo9Rq1qMB2qiRorNJtJAdxguu5TMvBHOc/F370KvqAe9t48vYp+/TbPKRNrMh/J55tOUmkuIqstZaew==", + "dev": true, + "requires": { + "esbuild": "^0.14.27", + "fsevents": "~2.3.2", + "postcss": "^8.4.13", + "resolve": "^1.22.0", + "rollup": "^2.59.0" + } + }, + "w3c-keyname": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.4.tgz", + "integrity": "sha512-tOhfEwEzFLJzf6d1ZPkYfGj+FWhIpBux9ppoP3rlclw3Z0BZv3N7b7030Z1kYth+6rDuAsXUFr+d0VE6Ed1ikw==" + }, + "which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + }, + "which-boxed-primitive": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz", + "integrity": "sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==", + "dev": true, + "requires": { + "is-bigint": "^1.0.1", + "is-boolean-object": "^1.1.0", + "is-number-object": "^1.0.4", + "is-string": "^1.0.5", + "is-symbol": "^1.0.3" + } + }, + "which-module": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz", + "integrity": "sha512-B+enWhmw6cjfVC7kS8Pj9pCrKSc5txArRyaYGe088shv/FGWH+0Rjx/xPgtsWfsUtS27FkP697E4DDhgrgoc0Q==" + }, + "word-wrap": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", + "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==", + "dev": true + }, + "wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "requires": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + } + } + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" + }, + "xstate": { + "version": "4.32.1", + "resolved": "https://registry.npmjs.org/xstate/-/xstate-4.32.1.tgz", + "integrity": "sha512-QYUd+3GkXZ8i6qdixnOn28bL3EvA++LONYL/EMWwKlFSh/hiLndJ8YTnz77FDs+JUXcwU7NZJg7qoezoRHc4GQ==" + }, + "xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "dev": true + }, + "y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true + }, + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, + "yaml": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", + "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==" + }, + "yargs": { + "version": "17.5.1", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.5.1.tgz", + "integrity": "sha512-t6YAJcxDkNX7NFYiVtKvWUz8l+PaKTLiL63mJYWR2GnHq2gjEWISzsLp9wg3aY36dY1j+gfIEL3pIF+XlJJfbA==", + "dev": true, + "requires": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.0.0" + } + }, + "yargs-parser": { + "version": "21.0.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.0.1.tgz", + "integrity": "sha512-9BK1jFpLzJROCI5TzwZL/TU4gqjK5xiHV/RfWLOahrjAko/e4DJkRDZQXfvqAsiZzzYhgAzbgz6lg48jcm4GLg==", + "dev": true + }, + "yup": { + "version": "0.32.11", + "resolved": "https://registry.npmjs.org/yup/-/yup-0.32.11.tgz", + "integrity": "sha512-Z2Fe1bn+eLstG8DRR6FTavGD+MeAwyfmouhHsIUgaADz8jvFKbO/fXc2trJKZg+5EBjh4gGm3iU/t3onKlXHIg==", + "requires": { + "@babel/runtime": "^7.15.4", + "@types/lodash": "^4.14.175", + "lodash": "^4.17.21", + "lodash-es": "^4.17.21", + "nanoclone": "^0.2.1", + "property-expr": "^2.0.4", + "toposort": "^2.0.2" + } + }, + "zustand": { + "version": "4.3.8", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-4.3.8.tgz", + "integrity": "sha512-4h28KCkHg5ii/wcFFJ5Fp+k1J3gJoasaIbppdgZFO4BPJnsNxL0mQXBSFgOgAdCdBj35aDTPvdAJReTMntFPGg==", + "requires": { + "use-sync-external-store": "1.2.0" + } + } + } +} diff --git a/izanami-frontend/package.json b/izanami-frontend/package.json new file mode 100644 index 000000000..4738e5432 --- /dev/null +++ b/izanami-frontend/package.json @@ -0,0 +1,55 @@ +{ + "name": "izanami-frontend", + "private": true, + "version": "0.0.0", + "scripts": { + "dev": "vite", + "build": "tsc && vite build && mkdir -p ../public && rm -rf ../public/* && cp -r ./dist/* ../public/", + "preview": "vite preview", + "test": "playwright test --workers 1 --trace on", + "screenshots": "npx tsx ./tests/screenshots/index.ts" + }, + "dependencies": { + "@emotion/react": "^11.11.1", + "@emotion/styled": "^11.11.0", + "@maif/react-forms": "1.6.3", + "@mui/material": "^5.13.6", + "@tanstack/react-table": "^8.1.3", + "@textea/json-viewer": "^3.1.1", + "@xstate/react": "^3.0.0", + "bootstrap": "^5.0.0", + "bootstrap-icons": "^1.8.2", + "classnames": "^2.3.0", + "date-fns": "^2.28.0", + "lodash": "^4.17.21", + "react": "18.1.0", + "react-dom": "18.1.0", + "react-hook-form": "^7.48.2", + "react-hot-toast": "^2.4.1", + "react-query": "^3.39.1", + "react-router-dom": "6.4.0", + "react-select": "^5.7.7", + "react-tooltip": "^5.14.0", + "uuid": "^8.3.2", + "xstate": "^4.32.1" + }, + "devDependencies": { + "@playwright/test": "^1.41.1", + "@types/jest": "^26.0.24", + "@types/node": "^16.3.0", + "@types/react-dom": "^18.0.5", + "@typescript-eslint/eslint-plugin": "^5.26.0", + "@typescript-eslint/parser": "^5.26.0", + "@vitejs/plugin-react": "^1.3.0", + "eslint": "^8.16.0", + "eslint-plugin-react": "^7.30.0", + "pg": "^8.11.3", + "prettier": "2.6.2", + "process": "^0.11.10", + "rollup-plugin-analyzer": "^4.0.0", + "rollup-plugin-visualizer": "^5.6.0", + "sass": "^1.55.0", + "typescript": "^4.6.3", + "vite": "^2.9.9" + } +} diff --git a/izanami-frontend/playwright.config.ts b/izanami-frontend/playwright.config.ts new file mode 100644 index 000000000..079733a64 --- /dev/null +++ b/izanami-frontend/playwright.config.ts @@ -0,0 +1,109 @@ +import { defineConfig, devices } from "@playwright/test"; +import path from "path"; + +export const STORAGE_STATE = path.join(__dirname, "playwright/.auth/user.json"); + +/** + * Read environment variables from file. + * https://github.com/motdotla/dotenv + */ +// require('dotenv').config(); + +/** + * See https://playwright.dev/docs/test-configuration. + */ +export default defineConfig({ + testDir: "./tests", + /* Run tests in files in parallel */ + fullyParallel: true, + /* Fail the build on CI if you accidentally left test.only in the source code. */ + forbidOnly: !!process.env.CI, + /* Retry on CI only */ + retries: process.env.CI ? 2 : 0, + /* Opt out of parallel tests on CI. */ + workers: process.env.CI ? 1 : undefined, + /* Reporter to use. See https://playwright.dev/docs/test-reporters */ + reporter: "html", + /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ + use: { + /* Base URL to use in actions like `await page.goto('/')`. */ + // baseURL: 'http://127.0.0.1:3000', + + /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ + trace: "on-first-retry", + baseURL: "http://localhost:3000", + }, + + /* Configure projects for major browsers */ + // See https://playwright.dev/docs/test-global-setup-teardown for global & storageState + projects: [ + { + name: "setup", + testMatch: /global.setup\.ts/, + }, + { + name: "chromium", + dependencies: ["setup"], + use: { ...devices["Desktop Chrome"], storageState: STORAGE_STATE }, + }, + + /*{ + name: "firefox", + dependencies: ["setup"], + use: { ...devices["Desktop Firefox"], storageState: STORAGE_STATE }, + }, + + { + name: "webkit", + dependencies: ["setup"], + use: { ...devices["Desktop Safari"], storageState: STORAGE_STATE }, + },*/ + + /* Test against mobile viewports. */ + // { + // name: 'Mobile Chrome', + // use: { ...devices['Pixel 5'] }, + // }, + // { + // name: 'Mobile Safari', + // use: { ...devices['iPhone 12'] }, + // }, + + /* Test against branded browsers. */ + // { + // name: 'Microsoft Edge', + // use: { ...devices['Desktop Edge'], channel: 'msedge' }, + // }, + // { + // name: 'Google Chrome', + // use: { ...devices['Desktop Chrome'], channel: 'chrome' }, + // }, + ], + + /* Run your local dev server before starting the tests */ + // webServer: { + // command: 'npm run start', + // url: 'http://127.0.0.1:3000', + // reuseExistingServer: !process.env.CI, + // }, +}); + +/* + npx playwright test + Runs the end-to-end tests. + + npx playwright test --ui + Starts the interactive UI mode. + + npx playwright test --project=chromium + Runs the tests only on Desktop Chrome. + + npx playwright test example + Runs the tests in a specific file. + + npx playwright test --debug + Runs the tests in debug mode. + + npx playwright codegen + Auto generate tests with Codegen. +*/ diff --git a/izanami-frontend/safari-pinned-tab.svg b/izanami-frontend/safari-pinned-tab.svg new file mode 100644 index 000000000..78ff71db2 --- /dev/null +++ b/izanami-frontend/safari-pinned-tab.svg @@ -0,0 +1,68 @@ + + + + +Created by potrace 1.14, written by Peter Selinger 2001-2017 + + + + + diff --git a/izanami-frontend/sandbox.js b/izanami-frontend/sandbox.js new file mode 100644 index 000000000..cf7852273 --- /dev/null +++ b/izanami-frontend/sandbox.js @@ -0,0 +1,22 @@ +const pg = require("pg"); + +async function run() { + const Client = pg.Client; + + const client = new Client({ + host: "localhost", + port: 5432, + database: "postgres", + user: "postgres", + password: "postgres", + }); + await client.connect(); + + const res = await client.query("SELECT $1::text as message", [ + "Hello world!", + ]); + console.log(res.rows[0].message); // Hello world! + await client.end(); +} + +run(); diff --git a/izanami-frontend/site.webmanifest b/izanami-frontend/site.webmanifest new file mode 100644 index 000000000..de65106f4 --- /dev/null +++ b/izanami-frontend/site.webmanifest @@ -0,0 +1,19 @@ +{ + "name": "", + "short_name": "", + "icons": [ + { + "src": "/android-chrome-192x192.png", + "sizes": "192x192", + "type": "image/png" + }, + { + "src": "/android-chrome-256x256.png", + "sizes": "256x256", + "type": "image/png" + } + ], + "theme_color": "#ffffff", + "background_color": "#ffffff", + "display": "standalone" +} diff --git a/izanami-frontend/src/App.css b/izanami-frontend/src/App.css new file mode 100644 index 000000000..1cd9a46e7 --- /dev/null +++ b/izanami-frontend/src/App.css @@ -0,0 +1,54 @@ +.col.sortable { + cursor: pointer; +} + +.table-filter { + max-width: 150px; + width: 100%; +} + +header { + display: flex; + align-items: center; + justify-content: space-between; +} + +header .nav-link { + display: inline; +} + +.openid-separator { + display: flex; + align-items: center; +} + +.openid-separator::before, +.openid-separator::after { + display: inline-flex; + content: ""; + border-top: 0.1rem solid rgba(221, 221, 221, 0.25); + flex: 1; + margin: 0 12px; +} + +.feature-separator { + text-align: center; + font-weight: bold; + margin-top: 8px; + margin-bottom: 8px; +} + +.error-message { + color: red; +} + +.fieldset-form fieldset { + background-color: #535352; + border-radius: 10px; + padding: 20px; + margin-bottom: 12px; +} + +/*.fieldset-form fieldset:nth-of-type(2n) { + background-color: #414140; +}*/ diff --git a/izanami-frontend/src/App.tsx b/izanami-frontend/src/App.tsx new file mode 100644 index 000000000..723d1787b --- /dev/null +++ b/izanami-frontend/src/App.tsx @@ -0,0 +1,620 @@ +import React, { + Component, + ComponentClass, + FunctionComponent, + useContext, + useEffect, +} from "react"; + +import { + createBrowserRouter, + useSearchParams, + useParams, + useLocation, + useNavigate, + redirect, + RouterProvider, + Outlet, + NavLink, + Navigate, +} from "react-router-dom"; +import { Toaster } from "react-hot-toast"; + +import { Project } from "./pages/project"; +import { Menu } from "./pages/menu"; +import { Tenant } from "./pages/tenant"; +import { QueryClientProvider, useQuery } from "react-query"; +import queryClient from "./queryClient"; +import { Tag } from "./pages/tag"; +import { Login } from "./pages/login"; +import Keys from "./pages/keys"; +import { isAuthenticated } from "./utils/authUtils"; +import "./App.css"; +import { Configuration, TUser } from "./utils/types"; +import { TIzanamiContext, IzanamiContext } from "./securityContext"; +import { Topbar } from "./Topbar"; +import { Users } from "./pages/users"; +import { Settings } from "./pages/settings"; +import { Invitation } from "./pages/invitation"; +import { Profile } from "./pages/profile"; +import { ForgottenPassword } from "./pages/forgottenPassword"; +import { FormReset } from "./pages/formReset"; +import { Modal } from "./components/Modal"; +import { + MutationNames, + queryConfiguration, + queryStats, + queryTenants, + updateConfiguration, +} from "./utils/queries"; +import { TenantSettings } from "./pages/tenantSettings"; +import { HomePage } from "./pages/home"; +import { ProjectContexts } from "./pages/projectContexts"; +import { ProjectSettings } from "./pages/projectSettings"; +import { GlobalContexts } from "./pages/globalContexts"; +import { QueryBuilder } from "./pages/queryBuilder"; +import { GlobalContextIcon } from "./utils/icons"; +import { WasmScripts } from "./pages/wasmScripts"; +import { + differenceInDays, + differenceInMonths, + differenceInSeconds, +} from "date-fns"; +import { JsonViewer } from "@textea/json-viewer"; + +function Wrapper({ + element, + ...rest +}: { + element: FunctionComponent | ComponentClass; +}) { + const params = useParams(); + const searchParams = useSearchParams(); + const location = useLocation(); + const navigate = useNavigate(); + return React.createElement(element, { + ...rest, + ...params, + ...Object.fromEntries(searchParams[0]), + location, + navigate, + }); +} + +function redirectToLoginIfNotAuthenticated({ + request, +}: { + request: { url: string }; +}) { + const { pathname, search } = new URL(request.url); + if (!isAuthenticated()) { + return redirect(`/login?req=${encodeURI(`${pathname}${search}`)}`); + } +} + +const router = createBrowserRouter([ + { + path: "/login", + element: , + }, + { + path: "/password/_reset", + element: , + }, + { + path: "/forgotten-password", + element: , + }, + { + path: "/invitation", + element: , + }, + { + path: "/", + element: , + loader: redirectToLoginIfNotAuthenticated, + handle: { + crumb: () => ( + ""} to={"/home"}> + Home + + ), + }, + children: [ + { + path: "/", + element: , + }, + { + path: "/home", + element: , + }, + { + path: "/users", + element: , + handle: { + crumb: () => ( + ""} to={`/users`}> + Users + + ), + }, + }, + { + path: "/settings", + element: , + handle: { + crumb: () => ( + ""} to={`/settings`}> + Settings + + ), + }, + }, + { + path: "/profile", + element: , + handle: { + crumb: () => ( + ""} to={`/profile`}> + Profile + + ), + }, + }, + { + path: "/tenants/:tenant/", + handle: { + crumb: (data: any) => ( + ""} to={`/tenants/${data.tenant}`}> +  {data.tenant} + + ), + }, + children: [ + { + path: "/tenants/:tenant/contexts", + element: , + handle: { + crumb: (data: any) => ( + ""} + to={`/tenants/${data.tenant}/contexts`} + > + +  Global contexts + + ), + }, + }, + { + path: "/tenants/:tenant/projects/:project", + handle: { + crumb: (data: any) => ( + ""} + to={`/tenants/${data.tenant}/projects/${data.project}`} + > +   + {data.project} + + ), + }, + children: [ + { + path: "/tenants/:tenant/projects/:project/", + element: , + }, + { + path: "/tenants/:tenant/projects/:project/contexts", + element: , + handle: { + crumb: (data: any) => ( + ""} + to={`/tenants/${data.tenant}/projects/${data.project}/contexts`} + > + +  Contexts + + ), + }, + }, + { + path: "/tenants/:tenant/projects/:project/settings", + element: , + handle: { + crumb: (data: any) => ( + ""} + to={`/tenants/${data.tenant}/projects/${data.project}/settings`} + > +  Settings + + ), + }, + }, + ], + }, + { + path: "/tenants/:tenant/keys", + element: , + handle: { + crumb: (data: any) => ( + ""} + to={`/tenants/${data.tenant}/keys`} + > +  {data.project} + Keys + + ), + }, + }, + { + path: "/tenants/:tenant/settings", + element: , + handle: { + crumb: (data: any) => ( + ""} + to={`/tenants/${data.tenant}/settings`} + > + Settings + + ), + }, + }, + { + path: "/tenants/:tenant/query-builder", + element: , + handle: { + crumb: (data: any) => ( + ""} + to={`/tenants/${data.tenant}/query-builder`} + > + Query + builder + + ), + }, + }, + { + path: "/tenants/:tenant/scripts", + element: , + handle: { + crumb: (data: any) => ( + ""} + to={`/tenants/${data.tenant}/scripts`} + > + Wasm scripts + + ), + }, + }, + { + path: "/tenants/:tenant/", + element: , + }, + { + path: "/tenants/:tenant/tags/:tag", + element: , + handle: { + crumb: (data: any) => ( + ""} + to={`/tenants/${data.tenant}/tags/${data.tag}`} + > + # {data.tag} + + ), + }, + }, + ], + }, + ], + }, + { + path: "*", + element: ( +
+

There's nothing here!

+
+ ), + }, +]); + +function RedirectToFirstTenant(): JSX.Element { + const context = useContext(IzanamiContext); + const defaultTenant = context.user?.defaultTenant; + let tenant = defaultTenant || context.user?.rights?.tenants?.[0]; + const tenantQuery = useQuery(MutationNames.TENANTS, () => queryTenants()); + + if (tenant) { + return ; + } else if (tenantQuery.data && tenantQuery.data.length > 0) { + return ; + } else if (tenantQuery.isLoading) { + return
Loading...
; + } else { + return ; + } +} + +function Layout() { + const { user, setUser, logout, expositionUrl, setExpositionUrl } = + useContext(IzanamiContext); + const loading = !user?.username || !expositionUrl; + useEffect(() => { + if (!user?.username) { + fetch("/api/admin/users/rights") + .then((response) => response.json()) + .then((user) => setUser(user)) + .catch(console.error); + } + if (!expositionUrl) { + fetch("/api/admin/exposition") + .then((response) => response.json()) + .then(({ url }) => setExpositionUrl(url)); + } + }, [user?.username]); + + if (loading) { + return
Loading...
; + } + + return ( +
+ {/*TOOD externalsier la navbar*/} +
+