[
  {
    "path": ".github/workflows/codepreview.yml",
    "content": "name: Create preview environment\n\non:\n  pull_request:\n    branches: [ master ]\n  push:\n    branches: [ master, scala3 ]\n\nconcurrency:\n  # The preview script can't handle concurrent deploys\n  group: codepreview\n  cancel-in-progress: false\n\n# TODO: Define minimal permissions, I haven't found which one is necessary to allow writing comments on commits\n# see https://docs.github.com/en/actions/using-jobs/assigning-permissions-to-jobs\n#permissions:\n#  contents: read # for checkout\n\njobs:\n  preview:\n\n    runs-on: ubuntu-latest\n\n    steps:\n      - name: Install ansible\n        run: python3 -m pip install --user ansible\n\n      - uses: coursier/cache-action@v6\n      - uses: VirtusLab/scala-cli-setup@main\n\n      - name: checkout\n        uses: actions/checkout@v2\n\n      - name: Setup Scala\n        uses: japgolly/setup-everything-scala@v3.1\n        with:\n          java-version: 'adopt:1.11.0-11'\n          node-version: '16.7.0'\n\n      - name: Cache compiled code\n        uses: actions/cache@v3\n        with:\n          path: |\n            **/target/\n            /home/runner/.ivy2/local\n          key: compiled-code-preview-cache-${{ hashFiles('**/build.sbt') }}\n          restore-keys: compiled-code-preview-cache-\n\n      - name: Compile\n        run: sbt compile\n\n      - name: Create SSH key\n        run: |\n          mkdir -p ~/.ssh/\n          echo \"$CODEPREVIEW_PRIVATE_KEY\" > ~/.ssh/id_rsa\n          chmod 600 ~/.ssh/id_rsa\n          echo \"StrictHostKeyChecking=no\" > ~/.ssh/config\n        shell: bash\n        env:\n          CODEPREVIEW_PRIVATE_KEY: ${{ secrets.CODEPREVIEW_PRIVATE_KEY }}\n\n      - name: Create codepreview scripts\n        run: |\n          rm -rf ./infra\n          curl -u \"github:$CODEPREVIEW_TOKEN\" -O https://sssppa.wiringbits.dev/sssppa.zip\n          unzip sssppa.zip -d .\n          chmod +x ./infra/scripts/*.sh\n        shell: bash\n        env:\n          CODEPREVIEW_TOKEN: ${{ secrets.CODEPREVIEW_TOKEN }}\n\n      - name: Create preview env\n        run: cd infra && ./scripts/deploy-preview.sh\n        env:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n          PR_NUMBER: ${{ github.event.number }}\n\n"
  },
  {
    "path": ".github/workflows/pull_request.yml",
    "content": "name: Build the app\n\non:\n  push:\n    branches: [ master ]\n  pull_request:\n    branches: [ master ]\n\nconcurrency:\n  # Only run once for latest commit per ref and cancel other (previous) runs.\n  group: ci-${{ github.ref }}\n  cancel-in-progress: true\n\npermissions:\n  contents: read # for checkout\n\njobs:\n  build:\n\n    runs-on: ubuntu-latest\n\n    steps:\n      - name: checkout\n        uses: actions/checkout@v2\n\n      - name: Setup Scala\n        uses: japgolly/setup-everything-scala@v3.1\n        with:\n          java-version: 'adopt:1.11.0-11'\n          node-version: '16.7.0'\n\n      - name: Cache compiled code\n        uses: actions/cache@v3\n        with:\n          path: |\n            **/target/\n            /home/runner/.ivy2/local\n          key: compiled-code-cache-${{ hashFiles('**/build.sbt') }}\n          restore-keys: compiled-code-cache-\n\n      - name: Check code format\n        run: sbt scalafmtCheckAll\n\n      - name: Compile\n        run: CI=true sbt compile\n\n      - name: Run tests\n        run: CI=true sbt test\n\n      - name: Test summary\n        if: always() # Always run, even if previous steps failed\n        uses: test-summary/action@v2\n        with:\n          paths: \"**/target/test-reports/*.xml\"\n\n      - name: Prepare web build\n        run: sbt web/build\n\n      - name: Prepare server build\n        run: sbt server/dist\n"
  },
  {
    "path": ".gitignore",
    "content": "target/\n.idea/\n.bsp/\n.vscode/\nlogs/\nadmin/build/\nweb/build/\nlocal.env\n\n# https://scalameta.org/metals/docs/editors/vscode/#files-and-directories-to-include-in-your-gitignore\n.metals/\n.bloop/\n.ammonite/\nmetals.sbt\n"
  },
  {
    "path": ".nvmrc",
    "content": "16.7.0\n\n"
  },
  {
    "path": ".sbtopts",
    "content": "-J-Xmx4G\n-J-XX:MaxMetaspaceSize=4G\n-J-XX:+CMSClassUnloadingEnabled\n"
  },
  {
    "path": ".scalafmt.conf",
    "content": "version = 3.7.3\nproject.git = true\nproject.excludeFilters = [\n]\n\nrunner.dialect=scala3\n\nmaxColumn = 120\nassumeStandardLibraryStripMargin = false\n\ncontinuationIndent.callSite = 2\ncontinuationIndent.defnSite = 4\n\nalign.preset = none\n\nonTestFailure = \"To fix this, run 'sbt scalafmt' from the project directory, to avoid this issue, ensure you set up IntelliJ to format the code using scalafmt, see https://scalameta.org/scalafmt/docs/installation.html#intellij\"\n"
  },
  {
    "path": ".sdkmanrc",
    "content": "java=11.0.16-tem\n"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2021 wiringbits\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "# Wiringbits Web Application Template\n\n![wiringbits](https://github.com/wiringbits/scala-webapp-template/workflows/Build%20the%20server%20app/badge.svg)\n[![Scala.js](https://www.scala-js.org/assets/badges/scalajs-1.6.0.svg)](https://www.scala-js.org)\n\nThis is the skeleton used by Wiringbits when creating new web applications in Scala/Scala.js, so far, we have created ~10 projects from this template, back-porting useful details to improve it.\n\nIf you require building a Web Application in Scala while you do not have the time to do many technical choices, this template could be a reasonable choice.\n\n\n## Why?\n\nScala has a common misconception, many people believe that it is hard to get productive with it, at Wiringbits, we have proven the contrary with this template. Engineers with no previous Scala experience tend to start contributing simple bug fixes at their first week (including undergrad interns).\n\nOur template provides all the necessary boilerplate to get started fast when building a traditional web application.\n\nDon't waste your time evaluating every library required to build your web app, pick this template and go from there.\n\nUsing Scala.js not only save us considerable time, it also allows us to avoid many common issues, for example, all frontend/backend validations are in sync just because the code is the same.\n\n\n## Demo\n\nWe have a live demo so that you can get a taste on what our template provides.\n\n- [Web App](https://template-demo.wiringbits.net) showcases the web application intended for the general user, explore it and create an account to get an idea on what your users will experience.\n- [API Docs](https://template-demo.wiringbits.net/api/docs/index.html) showcases the [Swagger UI](https://swagger.io/tools/swagger-ui/) which can help to explore the API directly.\n\n### Short videos\n\nUsers app 1m demo:\n\n[![Users app 1m demo](./docs/assets/demo-video-01.png)](https://youtu.be/hURUK4NCGBk \"Users app 1m demo\")\n\nDeployment 2m demo:\n\n[![Deployment 2m demo](./docs/assets/demo-video-02.png)](https://youtu.be/cN599dMa9EA \"Deployment 2m demo\")\n\n\n## What's included?\n\n1. User registration and authentication; Including email verification, profile updates, password recovery, and, captcha for spam prevention.\n2. Integration with the React ecosystem, most libraries/components will work right away, while we use [Material UI](https://v3.mui.com/), you can switch to your preferred component library.\n3. PostgreSQL as the data store layer, which is a reasonable choice for most web applications.\n4. Practical components for testing your server-side code, writing tests for the Data/Api layer is real simple, no excuses accepted.\n5. Practical frontend utilities, for example, test your frontend forms easily, consistent UI when performing asynchronous actions (fetching/submitting data), etc.\n6. Typed data inputs, don't bother running simple validations to form data at the backend, accepted requests are already validated.\n7. Reasonable Continuous-Integration workflows, don't waste time reviewing code format, asking whether tests are passing or looking at jobs logs finding which test failed (thanks to [TestForest Dashboard](https://github.com/marketplace/actions/testforest-dashboard) action), Github Actions do this for you.\n8. A simple to follow architecture, including short-guides for doing common tasks. \n9. Deployment scripts to cloud instances, we believe in simplicity and most projects are fine with simple managed servers instead of containers/K8s/etc.\n\n## Get started\n\nRead the [docs](./docs/README.md) or watch our [onboarding videos](http://onboarding.wiringbits.net).\n\n\n## Presentations\n\nThere have been some presentations involing this project:\n\n- Jan 2023; [ScaLatin](https://scalac.io/scalatin/); [Creando aplicaciones web con Scala/Scala.js](https://www.youtube.com/watch?v=PqI8brUxCRg); [slides](http://scalatin2023.wiringbits.net)\n- Oct 2022; [ScalaCon](https://www.scalacon.org/); [A Practical Skeleton for Your Next Scala Scala js Web Application](https://www.youtube.com/watch?v=xWGMr0AsAMU)\n\n## Scala.js bundle size\nThese are code-size measurements from the deployed version at 5/Feb/2023, overall, Scala.js core is minimal, gzipped versions are usually less than 1Mb.\n\n### Web app\n\n![sssppa-web-code-size](./docs/assets/images/sssppa-web-code-size.png)\n\n## Hire us\n\nThe open source work we do is funded by our Scala/Scala.js consulting and development services, [schedule a call](http://alexis.wiringbits.net/) to hire us for your project.\n"
  },
  {
    "path": "build.sbt",
    "content": "import java.nio.file.Files\nimport java.nio.file.StandardCopyOption.REPLACE_EXISTING\n\nThisBuild / scalaVersion := \"3.3.0\"\nThisBuild / organization := \"net.wiringbits\"\n\nval playJson = \"3.0.1\"\nval sttp = \"3.8.15\"\nval webappUtils = \"0.7.2\"\nval anorm = \"2.7.0\"\nval enumeratum = \"1.7.2\"\nval scalaJavaTime = \"2.5.0\"\nval tapir = \"1.8.5\"\nval chimney = \"0.8.0-RC1\"\n\nval consoleDisabledOptions = Seq(\"-Werror\", \"-Ywarn-unused\", \"-Ywarn-unused-import\")\n\n/** Say just `build` or `sbt build` to make a production bundle in `build`\n  */\nlazy val build = TaskKey[File](\"build\")\n\nlazy val commonSettings: Project => Project = {\n  _.settings(\n    // Enable fatal warnings only when running in the CI\n    scalacOptions ++= {\n      sys.env\n        .get(\"CI\")\n        .filter(_.nonEmpty)\n        .map(_ => Seq(\"-Werror\"))\n        .getOrElse(Seq.empty[String])\n    },\n    Compile / compile / wartremoverErrors ++= List(\n      Wart.ArrayEquals,\n      //      Wart.Any,\n      //      Wart.AsInstanceOf,\n      //      Wart.ExplicitImplicitTypes,\n      Wart.IsInstanceOf,\n      Wart.JavaConversions,\n      //      Wart.JavaSerializable,\n      Wart.MutableDataStructures,\n      //      Wart.NonUnitStatements,\n      //      Wart.Nothing,\n      Wart.Null,\n      Wart.OptionPartial,\n      //      Wart.Overloading,\n      //      Wart.Product,\n      //      Wart.PublicInference,\n      Wart.Return,\n      //      Wart.Serializable,\n      //      Wart.StringPlusAny,\n      //      Wart.ToString,\n      Wart.TryPartial\n    )\n  )\n}\n// TODO: Reuse it in all projects\nlazy val baseServerSettings: Project => Project = {\n  _.settings(\n    scalacOptions ++= Seq(\n      \"-unchecked\",\n      \"-deprecation\",\n      \"-feature\"\n    ),\n    Compile / doc / scalacOptions ++= Seq(\"-no-link-warnings\"),\n    // Some options are very noisy when using the console and prevent us using it smoothly, let's disable them\n    Compile / console / scalacOptions ~= (_.filterNot(consoleDisabledOptions.contains))\n  )\n}\n\n// Used only by web projects\nlazy val baseWebSettings: Project => Project =\n  _.enablePlugins(ScalaJSPlugin)\n    .settings(\n      scalacOptions ++= Seq(\n        \"-deprecation\", // Emit warning and location for usages of deprecated APIs.\n        \"-explaintypes\", // Explain type errors in more detail.\n        \"-feature\", // Emit warning and location for usages of features that should be imported explicitly.\n        \"-unchecked\" // Enable additional warnings where generated code depends on assumptions.\n      ),\n      scalaJSUseMainModuleInitializer := true,\n      /* disabled because it somehow triggers many warnings */\n      scalaJSLinkerConfig := scalaJSLinkerConfig.value.withSourceMap(false),\n      /* for slinky */\n      libraryDependencies ++= Seq(\"me.shadaj\" %%% \"slinky-hot\" % \"0.7.3\"),\n      libraryDependencies ++= Seq(\n        \"io.github.cquiroz\" %%% \"scala-java-time\" % scalaJavaTime,\n        \"io.github.cquiroz\" %%% \"scala-java-time-tzdb\" % scalaJavaTime\n      ),\n      Test / fork := false, // sjs needs this to run tests\n      Test / requireJsDomEnv := true\n    )\n\n// Used only by the lib projects\n// TODO: This should go to commonSettings instead\nlazy val baseLibSettings: Project => Project = _.settings(\n  scalacOptions ++= Seq(\n    \"-deprecation\", // Emit warning and location for usages of deprecated APIs.\n    \"-encoding\",\n    \"utf-8\", // Specify character encoding used by source files.\n    \"-explaintypes\", // Explain type errors in more detail.\n    \"-feature\", // Emit warning and location for usages of features that should be imported explicitly.\n    \"-unchecked\" // Enable additional warnings where generated code depends on assumptions.\n  )\n)\n\n/** Implement the `build` task define above. Most of this is really just to copy the index.html file around.\n  */\nlazy val browserProject: Project => Project =\n  _.settings(\n    build := {\n      val artifacts = (Compile / fullOptJS / webpack).value\n      val artifactFolder = (Compile / fullOptJS / crossTarget).value\n      val jsFolder = baseDirectory.value / \"src\" / \"main\" / \"js\"\n      val distFolder = baseDirectory.value / \"build\"\n\n      distFolder.mkdirs()\n      artifacts.foreach { artifact =>\n        val target = artifact.data.relativeTo(artifactFolder) match {\n          case None => distFolder / artifact.data.name\n          case Some(relFile) => distFolder / relFile.toString\n        }\n\n        Files.copy(artifact.data.toPath, target.toPath, REPLACE_EXISTING)\n      }\n\n      // copy public resources\n      Files\n        .walk(jsFolder.toPath)\n        .filter(x => !Files.isDirectory(x))\n        .forEach(source => {\n          source.toFile.relativeTo(jsFolder).foreach { relativeSource =>\n            val dest = distFolder / relativeSource.toString\n            dest.getParentFile.mkdirs()\n            Files.copy(source, dest.toPath, REPLACE_EXISTING)\n          }\n        })\n\n      // link the proper js bundle\n      val indexFrom = baseDirectory.value / \"src/main/js/index.html\"\n      val indexTo = distFolder / \"index.html\"\n\n      val indexPatchedContent = {\n        import collection.JavaConverters._\n        Files\n          .readAllLines(indexFrom.toPath, IO.utf8)\n          .asScala\n          .map(_.replaceAllLiterally(\"-fastopt\", \"-opt\"))\n          .mkString(\"\\n\")\n      }\n\n      Files.write(indexTo.toPath, indexPatchedContent.getBytes(IO.utf8))\n      distFolder\n    }\n  )\n\n// specify versions for all of reacts dependencies\nlazy val reactNpmDeps: Project => Project =\n  _.settings(\n    stTypescriptVersion := \"3.9.3\",\n    stIgnore += \"react-proxy\",\n    Compile / npmDependencies ++= Seq(\n      \"react\" -> \"18.2.0\",\n      \"@types/react\" -> \"18.0.33\",\n      \"react-dom\" -> \"18.2.0\",\n      \"@types/react-dom\" -> \"18.0.11\",\n      \"csstype\" -> \"2.6.11\",\n      \"react-proxy\" -> \"1.1.8\",\n      \"@types/prop-types\" -> \"15.7.3\"\n    )\n  )\n\nlazy val withCssLoading: Project => Project =\n  _.settings(\n    /* custom webpack file to include css */\n    Compile / webpackConfigFile := Some((ThisBuild / baseDirectory).value / \"custom.webpack.config.js\"),\n    Test / webpackConfigFile := None, // it is important to avoid the custom webpack config in tests to get them passing\n    Compile / npmDevDependencies ++= Seq(\n      \"webpack-merge\" -> \"4.2.2\",\n      \"css-loader\" -> \"3.4.2\",\n      \"style-loader\" -> \"1.1.3\",\n      \"file-loader\" -> \"5.1.0\",\n      \"url-loader\" -> \"3.0.0\"\n    )\n  )\n\nlazy val bundlerSettings: Project => Project =\n  _.settings(\n    Compile / fastOptJS / webpackExtraArgs += \"--mode=development\",\n    Compile / fullOptJS / webpackExtraArgs += \"--mode=production\",\n    Compile / fastOptJS / webpackDevServerExtraArgs += \"--mode=development\",\n    Compile / fullOptJS / webpackDevServerExtraArgs += \"--mode=production\"\n  )\n\n// Used only by play-based projects\nlazy val playSettings: Project => Project = {\n  _.enablePlugins(PlayScala)\n    .disablePlugins(PlayLayoutPlugin)\n    .settings(\n      // docs are huge and unnecessary\n      Compile / doc / sources := Nil,\n      Compile / doc / scalacOptions ++= Seq(\n        \"-no-link-warnings\"\n      ),\n      // remove play noisy warnings\n      play.sbt.routes.RoutesKeys.routesImport := Seq.empty,\n      libraryDependencies ++= Seq(\n        guice,\n        evolutions,\n        jdbc,\n        ws,\n        \"com.google.inject\" % \"guice\" % \"5.1.0\"\n      ),\n      // test\n      libraryDependencies ++= Seq(\n        \"org.scalatestplus.play\" %% \"scalatestplus-play\" % \"6.0.0-M6\" % Test,\n        \"org.scalatestplus\" %% \"mockito-4-6\" % \"3.2.15.0\" % Test\n      )\n    )\n}\n\nlazy val common = (crossProject(JSPlatform, JVMPlatform) in file(\"lib/common\"))\n  .configure(baseLibSettings, commonSettings)\n  .jsConfigure(_.enablePlugins(ScalaJSPlugin, ScalaJSBundlerPlugin, ScalablyTypedConverterPlugin))\n  .settings(\n    libraryDependencies ++= Seq()\n  )\n  .jvmSettings(\n    libraryDependencies ++= Seq(\n      \"org.playframework\" %% \"play-json\" % playJson,\n      \"net.wiringbits\" %% \"webapp-common\" % webappUtils,\n      \"org.scalatest\" %% \"scalatest\" % \"3.2.16\" % Test\n    )\n  )\n  .jsSettings(\n    useYarn := true,\n    Test / fork := false, // sjs needs this to run tests\n    stUseScalaJsDom := true,\n    Compile / stMinimize := Selection.All,\n    libraryDependencies ++= Seq(\n      \"io.github.cquiroz\" %%% \"scala-java-time\" % scalaJavaTime,\n      \"org.playframework\" %%% \"play-json\" % playJson,\n      \"net.wiringbits\" %%% \"webapp-common\" % webappUtils,\n      \"org.scalatest\" %%% \"scalatest\" % \"3.2.16\" % Test,\n      \"com.beachape\" %%% \"enumeratum\" % enumeratum\n    )\n  )\n\n// shared apis\nlazy val api = (crossProject(JSPlatform, JVMPlatform) in file(\"lib/api\"))\n  .dependsOn(common)\n  .configure(baseLibSettings, commonSettings)\n  .jsConfigure(_.enablePlugins(ScalaJSPlugin, ScalaJSBundlerPlugin, ScalablyTypedConverterPlugin))\n  .jvmSettings(\n    libraryDependencies ++= Seq(\n      \"org.playframework\" %% \"play-json\" % playJson,\n      \"com.softwaremill.sttp.client3\" %% \"core\" % sttp,\n      \"com.softwaremill.sttp.tapir\" %% \"tapir-json-play\" % tapir,\n      \"com.softwaremill.sttp.tapir\" %% \"tapir-sttp-client\" % tapir\n    )\n  )\n  .jsSettings(\n    useYarn := true,\n    Test / fork := false, // sjs needs this to run tests\n    stUseScalaJsDom := true,\n    Compile / stMinimize := Selection.All,\n    libraryDependencies ++= Seq(\n      \"org.playframework\" %%% \"play-json\" % playJson,\n      \"org.scalatest\" %%% \"scalatest\" % \"3.2.16\" % Test,\n      \"com.beachape\" %%% \"enumeratum\" % enumeratum,\n      \"com.softwaremill.sttp.client3\" %%% \"core\" % sttp,\n      \"com.softwaremill.sttp.tapir\" %%% \"tapir-json-play\" % tapir,\n      \"com.softwaremill.sttp.tapir\" %%% \"tapir-sttp-client\" % tapir\n    )\n  )\n\n// shared on the ui only\nlazy val ui = (project in file(\"lib/ui\"))\n  .configure(baseLibSettings, commonSettings)\n  .configure(_.enablePlugins(ScalaJSPlugin, ScalaJSBundlerPlugin, ScalablyTypedConverterPlugin))\n  .dependsOn(api.js, common.js)\n  .settings(\n    name := \"wiringbits-lib-ui\",\n    useYarn := true,\n    Test / requireJsDomEnv := true,\n    Test / fork := false, // sjs needs this to run tests\n    stTypescriptVersion := \"3.9.3\",\n    // material-ui is provided by a pre-packaged library\n    stIgnore ++= List(\n      \"@mui/material\",\n      \"@mui/icons-material\",\n      \"@mui/joy\",\n      \"@emotion/react\",\n      \"@emotion/styled\",\n      \"react-router\",\n      \"react-router-dom\"\n    ),\n    Compile / npmDependencies ++= Seq(\n      \"@mui/material\" -> \"5.11.16\",\n      \"@mui/icons-material\" -> \"5.11.16\",\n      \"@mui/joy\" -> \"5.0.0-alpha.74\",\n      \"@emotion/react\" -> \"11.10.6\",\n      \"@emotion/styled\" -> \"11.10.6\",\n      \"react-router\" -> \"5.1.2\",\n      \"react-router-dom\" -> \"5.1.2\"\n    ),\n    stFlavour := Flavour.Slinky,\n    stReactEnableTreeShaking := Selection.All,\n    stUseScalaJsDom := true,\n    Compile / stMinimize := Selection.All,\n    libraryDependencies ++= Seq(\n      \"io.github.cquiroz\" %%% \"scala-java-time\" % scalaJavaTime,\n      \"org.scala-js\" %%% \"scala-js-macrotask-executor\" % \"1.1.1\",\n      \"com.olvind.st-material-ui\" %%% \"st-material-ui-icons-slinky\" % \"5.11.16\",\n      \"net.wiringbits\" %%% \"slinky-utils\" % webappUtils,\n      \"org.scalatest\" %%% \"scalatest\" % \"3.2.16\" % Test,\n      \"com.beachape\" %%% \"enumeratum\" % enumeratum\n    )\n  )\n\nlazy val server = (project in file(\"server\"))\n  .dependsOn(common.jvm, api.jvm)\n  .configure(baseServerSettings, commonSettings, playSettings)\n  .settings(\n    name := \"wiringbits-server\",\n    fork := true,\n    Test / fork := true, // allows for graceful shutdown of containers once the tests have finished running\n    libraryDependencies ++= Seq(\n      \"org.playframework.anorm\" %% \"anorm\" % anorm,\n      \"org.playframework.anorm\" %% \"anorm-postgres\" % anorm,\n      \"org.playframework\" %% \"play-json\" % playJson,\n      \"org.postgresql\" % \"postgresql\" % \"42.6.0\",\n      \"de.svenkubiak\" % \"jBCrypt\" % \"0.4.3\",\n      \"commons-validator\" % \"commons-validator\" % \"1.7\",\n      \"com.dimafeng\" %% \"testcontainers-scala-scalatest\" % \"0.40.16\" % \"test\",\n      \"com.dimafeng\" %% \"testcontainers-scala-postgresql\" % \"0.40.16\" % \"test\",\n      \"com.softwaremill.sttp.client3\" %% \"core\" % sttp % \"test\",\n      \"com.softwaremill.sttp.client3\" %% \"async-http-client-backend-future\" % sttp % \"test\",\n      // \"net.wiringbits\" %% \"admin-data-explorer-play-server\" % webappUtils,\n      \"software.amazon.awssdk\" % \"ses\" % \"2.17.141\",\n      \"jakarta.xml.bind\" % \"jakarta.xml.bind-api\" % \"4.0.0\",\n      \"org.apache.commons\" % \"commons-text\" % \"1.10.0\",\n      // JAX-B dependencies for JDK 9+, required to use play sessions\n      \"javax.xml.bind\" % \"jaxb-api\" % \"2.3.1\",\n      \"javax.annotation\" % \"javax.annotation-api\" % \"1.3.2\",\n      \"javax.el\" % \"javax.el-api\" % \"3.0.0\",\n      \"org.glassfish\" % \"javax.el\" % \"3.0.0\",\n      \"com.beachape\" %% \"enumeratum\" % enumeratum,\n      \"io.scalaland\" %% \"chimney\" % chimney,\n      \"com.softwaremill.sttp.tapir\" %% \"tapir-swagger-ui-bundle\" % tapir,\n      \"com.softwaremill.sttp.tapir\" %% \"tapir-json-play\" % tapir,\n      \"com.softwaremill.sttp.tapir\" %% \"tapir-play-server\" % tapir,\n      \"org.apache.pekko\" %% \"pekko-stream\" % \"1.0.1\"\n    )\n  )\n\nlazy val webBuildInfoSettings: Project => Project = _.enablePlugins(BuildInfoPlugin)\n  .settings(\n    buildInfoKeys := Seq[BuildInfoKey](name, version, scalaVersion, sbtVersion),\n    buildInfoKeys ++= {\n      val apiUrl = sys.env.get(\"API_URL\")\n      val values = Seq(\n        \"apiUrl\" -> apiUrl\n      )\n      // Logging these values is useful to make sure that the necessary settings\n      // are being overriden when packaging the app.\n      sLog.value.info(s\"BuildInfo settings:\\n${values.mkString(\"\\n\")}\")\n      values.map(t => BuildInfoKey(t._1, t._2))\n    },\n    buildInfoPackage := \"net.wiringbits\",\n    buildInfoUsePackageAsPath := true\n  )\n\nlazy val web = (project in file(\"web\"))\n  .dependsOn(common.js, api.js, ui)\n  .enablePlugins(ScalablyTypedConverterPlugin)\n  .configure(\n    baseWebSettings,\n    browserProject,\n    commonSettings,\n    reactNpmDeps,\n    withCssLoading,\n    bundlerSettings,\n    webBuildInfoSettings\n  )\n  .settings(\n    name := \"wiringbits-web\",\n    useYarn := true,\n    webpackDevServerPort := 8080,\n    stFlavour := Flavour.Slinky,\n    stReactEnableTreeShaking := Selection.All,\n    stUseScalaJsDom := true,\n    Compile / stMinimize := Selection.All,\n    // material-ui is provided by a pre-packaged library\n    stIgnore ++= List(\"@mui/material\", \"@mui/icons-material\", \"@mui/joy\", \"react-router\", \"react-router-dom\"),\n    Compile / npmDependencies ++= Seq(\n      \"@mui/material\" -> \"5.11.16\",\n      \"@mui/icons-material\" -> \"5.11.16\",\n      \"@mui/joy\" -> \"5.0.0-alpha.74\",\n      \"@emotion/styled\" -> \"11.10.6\",\n      \"@emotion/react\" -> \"11.10.6\",\n      \"react-router\" -> \"5.1.2\",\n      \"react-router-dom\" -> \"5.1.2\",\n      \"react-google-recaptcha\" -> \"2.1.0\",\n      \"@types/react-google-recaptcha\" -> \"2.1.0\"\n    ),\n    libraryDependencies ++= Seq(\n      \"org.playframework\" %%% \"play-json\" % playJson,\n      \"com.softwaremill.sttp.client3\" %%% \"core\" % sttp,\n      \"org.scala-js\" %%% \"scala-js-macrotask-executor\" % \"1.1.1\",\n      \"com.olvind.st-material-ui\" %%% \"st-material-ui-icons-slinky\" % \"5.11.16\",\n      \"io.monix\" %%% \"monix-reactive\" % \"3.4.1\",\n      \"com.softwaremill.sttp.tapir\" %%% \"tapir-sttp-client\" % tapir\n    ),\n    libraryDependencies ++= Seq(\n      \"org.scalatest\" %%% \"scalatest\" % \"3.2.16\" % Test\n    )\n  )\n\nlazy val root = (project in file(\".\"))\n  .aggregate(\n    common.jvm,\n    common.js,\n    api.jvm,\n    api.js,\n    ui,\n    server,\n    web\n  )\n  .settings(\n    publish := {},\n    publishLocal := {}\n  )\n\naddCommandAlias(\"dev-web\", \";web/fastOptJS::startWebpackDevServer;~web/fastOptJS\")\n"
  },
  {
    "path": "custom.webpack.config.js",
    "content": "var path = require(\"path\");\nvar merge = require('webpack-merge');\nvar generated = require('./scalajs.webpack.config');\n\nvar local = {\n    devServer: {\n        // the historyAPIFallback allows react-router to work\n        historyApiFallback: true,\n        proxy: {\n            // when a request to /api is done, we want to apply a proxy\n            '/api': {\n                changeOrigin: true,\n                cookieDomainRewrite: 'localhost',\n                target: 'http://localhost:9000',\n                pathRewrite: { '^/api': '/'},\n                onProxyReq: (req) => {\n                    if (req.getHeader('origin')) {\n                        req.setHeader('origin', 'http://localhost:9000')\n                    }\n                }\n            }\n        }\n    },\n    resolve: {\n        alias: {\n            \"js\": path.resolve(__dirname, \"../../../../src/main/js\"),\n        }\n    },\n    module: {\n        rules: [\n            {\n                test: /\\.css$/,\n                use: ['style-loader', 'css-loader']\n            },\n            {\n                test: /\\.(ttf|eot|woff|png|jpg|glb|svg)$/,\n                use: 'file-loader'\n            },\n            {\n                test: /\\.(eot)$/,\n                use: 'url-loader'\n            }\n        ]\n    }\n}\n\nmodule.exports = merge(generated, local);\n"
  },
  {
    "path": "docs/README.md",
    "content": "# Wiringbits Scala WebApp template - Docs\n\n- [Setup development environment](./setup-dev-environment.md)\n- [Architecture](./architecture.md)\n- [Design desicions](./design-decisions.md)\n- [Learning material](./learning-material.md)\n- [Swagger integration](./swagger-integration.md).\n\n\n## Diagrams\n\nThe docs include diagrams created with plantuml, you will need to compile them after they are updated.\n\nBefore you can compile those, you will require some dependencies:\n\n1. `graphviz` which can be installed with `apt install graphviz`\n2. Download `plantuml.jar` from the official [site](https://plantuml.com/starting).\n\nThen, you can execute this to generate them all:\n- `java -jar ~/Downloads/plantuml.jar ./diagram-sources -o ../assets/diagrams/`\n\nConsider using the [plantuml plugin for IntelliJ](https://plugins.jetbrains.com/plugin/7017-plantuml-integration/) when editing diagrams, this way, you can preview the changes.\n"
  },
  {
    "path": "docs/architecture.md",
    "content": "# Architecture\n\nThe following diagrams illustrate the overall project architecture.\n\n**Disclaimer** There are some parts of the code (specially `server`) that do not fit the architecture completely, we plan to get there.\n\n## Infrastructure\n![Infrastructure architecture diagram](./assets/diagrams/architecture-infra.png)\n\nThe [infra](../infra) project includes Ansible scripts to configure your own Server, be sure to check it out.\n\nSummary:\n\n- The app is usually hosted on a cloud server, let it be a DigitalOcean Droplet, an AWS EC2 instance, etc.\n- Ubuntu 20.04 is the target OS, newer versions would likely work without issues.\n- We recommend a managed database for Postgres.\n- While the diagram shows all components in a single server, we can easily separate nginx as a load balancer + many server instances.\n- Frontend apps are composed by static assets, stored at the server filesystem.\n- `nginx` is the entry point handling user requests, which has a TLS certificate (thanks to LetsEncrypt/CertBot), depending on the domain, it serves the files for the regular app.\n- When the server API is being invoked, `nginx` will route the traffic to the server app.\n- The server app connects to postgres and external services when necessary (like AWS SES).\n- AWS SES is being used to send emails.\n\n\n## Modules\n![Modules architecture diagram](./assets/diagrams/architecture-modules.png)\n\nThe application modules make sure to share code when possible, some of them cross-compile to Scala.js:\n\n- [LibCommon](../lib/common) has code shared over the whole application (Scala/Scala.js), it consists mostly of models.\n- [LibAPI](../lib/api) has code shared to the `server` (Scala) and the web apps (Scala.js), it includes all the models defining the request/response for the `server` endpoints, it also includes the API client that web apps use to invoke the `server` app.\n- [LibUI](../lib/ui) has code shared between the webapps (Scala.js), while it is mostly empty, it can include reusable components for the UI.\n- [Web](../web) has the web app code for the regular user application (Scala.js).\n- [Server](../server) has the code for the server app (Scala).\n\n\n## Server\n![Server architecture diagram](./assets/diagrams/architecture-server.png)\n\nThe server architecture is a mix taking advantage from Domain Driven Design (DDD), Hexagonal Architecture, and, Clean Architecture.\n\n**NOTE** While current diagrams do not display it, there is are some core models that are shared on all layers.\n\nLet's visit the layers from the top to the bottom.\n\n### Controllers\n![Controllers architecture diagram](./assets/diagrams/architecture-server-controllers.png)\n\nControllers is the entry point layer for the user requests, it is tied to the http-framework, it has these responsibilities:\n\n- Decode requests into typed models.\n- Authenticate requests.\n- Delegate the work to the actions layer.\n- Encode responses.\n\n### Actions\n![Actions architecture diagram](./assets/diagrams/architecture-server-actions.png)\n\nActions is the non-framework entrypoint, for example, if the http-layer gets another implementation, there shouldn't be a need to update Actions, it has these responsibilities:\n\n- Actions have 0 knowledge about anything on the `Controllers` layer.\n- Each action is represented by a class/file, which has a single public method, commonly called `apply` so that Scala syntactic sugar can be used, like `getCurrentUserAction()`.\n- An action can’t depend on another action, when we get to such need, we would usually extract the common functionality into a service which can be invoked from many actions.\n- Checks authorization rules.\n- Runs complex validations (like the ones requiring interactions with another layers, checking whether an email is already registered could be one example).\n- Usually, an action would combine work from other layers, like reading data from a Repository and submitting it to an External API.\n\n\n### Services\n![Services architecture diagram](./assets/diagrams/architecture-server-services.png)\n\nThe Services layer is composed of business rules \n\nCombines the work from Repositories and ExternalApis\n\nExpose functions handling business rules when they get complex enough to fit in Actions:\n\n- Services have 0 knowledge about Actions/Controllers.\n- Combines the work from Repositories and ExternalApis\n\n\n\n\n### External APIs\n![External APIs architecture diagram](./assets/diagrams/architecture-server-external-apis.png)\n\nExternal APIs layer holds anything necessary to communicate with external services, for example, AWS SES.\n\nWhen working on this layer, it is a good idea to consider that any changes should be extractable into an isolated library, hence, business rules must not go here.\n\nOf course, this layer won't know anything about Controllers/Actions/Services/Repositories.\n\n\n### Repositories\n![Repositories architecture diagram](./assets/diagrams/architecture-server-repositories.png)\n\nRepositories is the entry point to the storage layer, given that we use Postgres, this layer is in charge of choosing when an operation requires a transaction.\n\nGiven the transactional capabilities, there are times when a repository could deal with small business rules, the ones that matter for data integrity. For example, creating a user could require to store a token used to verify the user's email.\n\nDetails:\n\n- The work done by Repositories is mostly composing DAOs.\n- Repositories do not know any other layer besides DAOs.\n- This layer is **NOT** responsible of validating input format.\n- This layer could validate whether an item already exists.\n\n### DAOs\n![DAOs architecture diagram](./assets/diagrams/architecture-server-daos.png)\n\nThe DAOs (Data-Access-Object) layer is the one that knows how to deal with database tables/rows, transforming operations into SQL statements, as well as parsing results into data models.\n\nDAOs don't know about any other layer.\n"
  },
  {
    "path": "docs/design-decisions.md",
    "content": "# Design decisions\nThis document explains why we took certain design decisions on how the project is built/structured.\n\n## 2022/Aug - Avoid default parameter in most cases\nWe commonly deal with models that are similar but belong to a different domain, [chimney](https://scalalandio.github.io/chimney) help us to transform those models from one domain to another, while this tool is handy, it does not play well with default values in arguments.\n\nTake this snippet as an example:\n\n```scala\ncase class CreateUserApiRequest(name: String, age: Option[Int])\n\ncase class CreateUserData(name: String, yearsOld: Option[Int] = None)\n\ndef transform(request: CreateUserApiRequest): CreateUserData = request.into[CreateUserData].transform\n```\n\nWhile the `transform` function would succeed, the `age` value will never become the `yearsOld` value, if there wasn't a default value, we'd get a compile error which would give us a chance to fix the problem (`request.into[CreateUserData].withFieldRenamed(_.age, _.yearsOld).transform`).\n\nStill, there can be exceptions:\n- The http API layer usually gets default values when adding a new parameter to an API method, this way, we keep backwards compatibility to support old API clients.\n\n\n## 2022/Apr - Naming conventions for api/data models\n\nThe project follows some principles from DDD (Domain Driven Design), we use different models for different layers even if they look quite similar.\n\nFor example, when creating an endpoint that creates a user, we'd end up with models like `models.api.CreateUser` and `models.data.CreateUser`, in theory, we could be disciplined enough to follow the conventions and refer to the models with the package from the domain we are interested in, like:\n\n```scala\nimport models._\n\ndef createUserApi(model: api.CreateUser)\ndef createUserData(model: data.CreateUser)\n```\n\nUnfortunately, IDE's automatically import the models from specific packages, which commonly causes conflicts because IDE imported the data model while we require the api one, there are pieces where we even need to deal with both.\n\nThen, it seems more practical to just include the domain name at the model name instea, like `models.data.CreateUserData`, this way, IDE's won't have ambiguous choices.\n\n\n## 2022/Jan/23 - Avoid creating postgres extensions in evolution scripts\n\nWhile it is very handy to keep all the necessary sql operations at the evolution scripts, it is a good practice to limit the permissions for the database user, in fact, in AWS RDS, the default user won't be able to create some extensions. Solving such an issue can be annoying, hence, the pain is being shifted to the local environments instead.\n\nIn short, when creating a local database, you will see yourself creating extensions manually like `CREATE EXTENSION CITEXT;`\n\nRef: 0439d7b3159e01f886ceeb3f0ff0d2d471f5e304\n\n\n## Undated - Do not use `Downs` in evolutions\n\nFrom experience, downs can become annoying, imagine that you are trying branch `A` while another developer pushes a change to an existing evolution which includes downs, your data will be destroyed without any confirmation, on the other hand, when there are no downs, you will be required to throw away the data manually, in theory, we could get this behavior by updating `application.conf` but we have found that avoiding downs tend to work better.\n"
  },
  {
    "path": "docs/diagram-sources/architecture-infra.puml",
    "content": "@startuml\nTitle Wiringbits Scala WebApp Template - Infrastructure Diagram\n\nskinparam {\n    ArrowColor Red\n    linetype ortho\n}\n\ncloud Cloud {\n    database Postgres\n    rectangle UbuntuServer {\n        node ScalaServerApp\n        node nginx\n\n        folder FileSystem {\n            file WebAssets\n            file AdminAssets\n        }\n    }\n\n    component EmailService\n    ScalaServerApp -> EmailService\n    nginx -> WebAssets\n    nginx --> AdminAssets\n    nginx --> ScalaServerApp\n    ScalaServerApp --> Postgres\n}\n\nperson RegularUser\nperson AdminUser\n\nRegularUser -> nginx\nAdminUser --> nginx\n@enduml"
  },
  {
    "path": "docs/diagram-sources/architecture-modules.puml",
    "content": "@startuml\nTitle Wiringbits Scala WebApp Template - Module Diagram\n\nskinparam {\n    ArrowColor Red\n}\n\npackage LibCommon {\n    [Typed models shared everywhere\\n* Scala/Scala.js]\n}\n\npackage LibUI {\n    [Code shared on UI apps (web/admin)\\n* Scala.js]\n}\n\npackage LibAPI {\n    [REST API client and models\\n* Scala/Scala.js]\n}\n\npackage WebApp {\n    [The main web app\\n* Scala.js]\n}\npackage AdminApp {\n    [The admin web app\\n* Scala.js]\n}\npackage ServerApp {\n    [The server side app\\n* Scala]\n}\n\nWebApp .left....> LibUI : uses\nWebApp .left....> LibAPI : uses\n\nAdminApp .right.> LibUI : uses\nAdminApp .right.> LibAPI : uses\n\nServerApp .> LibAPI : uses\n\nLibUI .up..> LibCommon : uses\nLibAPI .down.> LibCommon : uses\n@enduml\n"
  },
  {
    "path": "docs/diagram-sources/architecture-server-actions.puml",
    "content": "@startuml\nTitle Wiringbits Scala WebApp Template - Server Architecture - Actions\n\nskinparam {\n    linetype ortho\n}\n\nskinparam component {\n  BackgroundColor LightBlue\n}\nskinparam rectangle {\n  BackgroundColor White\n}\n\nrectangle Controllers {\n    component Actions {\n        rectangle Services {\n            rectangle ExternalApis {\n                rectangle ExternalApiClients {\n                    rectangle ExternalApiModels\n                }\n            }\n            rectangle Repositories {\n                rectangle DAOs {\n                    rectangle DataModels\n                }\n            }\n        }\n    }\n}\n@enduml\n"
  },
  {
    "path": "docs/diagram-sources/architecture-server-controllers.puml",
    "content": "@startuml\nTitle Wiringbits Scala WebApp Template - Server Architecture - Controllers\n\nskinparam {\n    linetype ortho\n}\n\nskinparam component {\n  BackgroundColor LightBlue\n}\nskinparam rectangle {\n  BackgroundColor White\n}\n\ncomponent Controllers {\n    rectangle Actions {\n        rectangle Services {\n            rectangle ExternalApis {\n                rectangle ExternalApiClients {\n                    rectangle ExternalApiModels\n                }\n            }\n            rectangle Repositories {\n                rectangle DAOs {\n                    rectangle DataModels\n                }\n            }\n        }\n    }\n}\n@enduml\n"
  },
  {
    "path": "docs/diagram-sources/architecture-server-daos.puml",
    "content": "@startuml\nTitle Wiringbits Scala WebApp Template - Server Architecture - DAOs\n\nskinparam {\n    linetype ortho\n}\n\nskinparam component {\n  BackgroundColor LightBlue\n}\nskinparam rectangle {\n  BackgroundColor White\n}\n\nrectangle Controllers {\n    rectangle Actions {\n        rectangle Services {\n            rectangle ExternalApis {\n                rectangle ExternalApiClients {\n                    rectangle ExternalApiModels\n                }\n            }\n            rectangle Repositories {\n                component DAOs {\n                    rectangle DataModels\n                }\n            }\n        }\n    }\n}\n@enduml\n"
  },
  {
    "path": "docs/diagram-sources/architecture-server-external-apis.puml",
    "content": "@startuml\nTitle Wiringbits Scala WebApp Template - Server Architecture - External APIs\n\nskinparam {\n    linetype ortho\n}\n\nskinparam component {\n  BackgroundColor LightBlue\n}\nskinparam rectangle {\n  BackgroundColor White\n}\n\nrectangle Controllers {\n    rectangle Actions {\n        rectangle Services {\n            component ExternalApis {\n                rectangle ExternalApiClients {\n                    rectangle ExternalApiModels\n                }\n            }\n            rectangle Repositories {\n                rectangle DAOs {\n                    rectangle DataModels\n                }\n            }\n        }\n    }\n}\n@enduml\n"
  },
  {
    "path": "docs/diagram-sources/architecture-server-repositories.puml",
    "content": "@startuml\nTitle Wiringbits Scala WebApp Template - Server Architecture - Repositories\n\nskinparam {\n    linetype ortho\n}\n\nskinparam component {\n  BackgroundColor LightBlue\n}\nskinparam rectangle {\n  BackgroundColor White\n}\n\nrectangle Controllers {\n    rectangle Actions {\n        rectangle Services {\n            rectangle ExternalApis {\n                rectangle ExternalApiClients {\n                    rectangle ExternalApiModels\n                }\n            }\n            component Repositories {\n                rectangle DAOs {\n                    rectangle DataModels\n                }\n            }\n        }\n    }\n}\n@enduml\n"
  },
  {
    "path": "docs/diagram-sources/architecture-server-services.puml",
    "content": "@startuml\nTitle Wiringbits Scala WebApp Template - Server Architecture - Services\n\nskinparam {\n    linetype ortho\n}\n\nskinparam component {\n  BackgroundColor LightBlue\n}\nskinparam rectangle {\n  BackgroundColor White\n}\n\nrectangle Controllers {\n    rectangle Actions {\n        component Services {\n            rectangle ExternalApis {\n                rectangle ExternalApiClients {\n                    rectangle ExternalApiModels\n                }\n            }\n            rectangle Repositories {\n                rectangle DAOs {\n                    rectangle DataModels\n                }\n            }\n        }\n    }\n}\n@enduml\n"
  },
  {
    "path": "docs/diagram-sources/architecture-server.puml",
    "content": "@startuml\nTitle Wiringbits Scala WebApp Template - Server Architecture\n\nskinparam {\n    linetype ortho\n}\n\nrectangle Controllers {\n    rectangle Actions {\n        rectangle Services {\n            rectangle ExternalApis {\n                rectangle ExternalApiClients {\n                    rectangle ExternalApiModels\n                }\n            }\n            rectangle Repositories {\n                rectangle DAOs {\n                    rectangle DataModels\n                }\n            }\n        }\n    }\n}\n@enduml"
  },
  {
    "path": "docs/learning-material.md",
    "content": "# Learning material\n\nThese are the tools used by the template, you don't need to master them all but being familiar with them definitely helps:\n- [Scala](https://scala-lang.org/), we use Scala 2.13 because it has great tooling support, we'll eventually upgrade to Scala 3.\n- [Scala.js](https://www.scala-js.org/) powers the frontend side.\n- [Scalablytyped](https://scalablytyped.org/) generates the Scala facades to interact with JavaScript libraries by converting TypeScript definitions to Scala.js facades.\n- [yarn](https://yarnpkg.com) (v1) as the JavaScript package manager.\n- [React](https://reactjs.org/) as the view library.\n- [Slinky](https://slinky.dev/) being the Scala wrapper for React.\n- [Webpack](https://webpack.js.org) to bundle the web apps.\n- [Scalajs bundler](https://scalacenter.github.io/scalajs-bundler/) being the Scala wrapper for Webpack.\n- [Material UI v3](https://v3.material-ui.com/) as the Material UI framework on top of React (hoping to upgrade to v5 when Scalablytyped supports it).\n- [Play Framework](https://playframework.com/) as the backend framework, used for the REST API.\n- [sttp](https://github.com/softwaremill/sttp/) as the REST API client.\n- [react-router](https://www.npmjs.com/package/react-router) is the frontend routing library.\n- [play-json](https://github.com/playframework/play-json/) is the JSON library.\n- [ansible](https://ansible.com/) as the tool for deploying everything to a VM.\n- [nginx](https://nginx.org/en/) as the reverse proxy for handling the internet traffic, as well as the authentication mechanism for admin endpoints.\n- [GitHub](https://github.com/features/actions) actions integration so that you have a way to get every commit tested.\n"
  },
  {
    "path": "docs/setup-dev-environment.md",
    "content": "# Setup development environment\n\nLet's get started setting up your development environment.\n\n**NOTE** The instructions will work better on Linux/Mac, there could be some details that do not work on Windows (help wanted).\n\nThere are demo videos while configuring a local environment in Ubuntu 22.04, covering everything, from the JDK install step until the application runs: http://onboarding.wiringbits.net \n\n**[Table of Contents](http://tableofcontent.eu)**\n\n- [Compile-time dependencies](compile-time-dependencies)\n- [Runtime dependencies](#runtime-dependencies)\n   - [Postgres](#postgres)\n   - [AWS Email Service](#aws-email-service)\n   - [direnv](#direnv)\n   - [Custom config](#custom-config)\n   - [Run](#run)\n- [Test dependencies](#test-dependencies)\n- [Deployment setup](#deployment-setup)\n\n## Compile-time dependencies\n\n1. Clone the repository\n\n    ```shell\n    git clone git@github.com:wiringbits/scala-webapp-template.git\n    ```\n\n2. JDK setup, we highly recommend [SDKMAN](https://sdkman.io/) due to its simplicity to switch between different jdk versions, run `sdk env` to pick the project's suggested jdk or edit sdkman config (`~/.sdkman/etc/config`) to set `sdkman_auto_env=true` which picks the project's jdk automatically:\n\n   ```shell\n   # sdkman_auto_env=true would pick the right jdk when moving into the project's directory\n   $ cd scala-webapp-template\n   \n   Using java version 11.0.16-tem in this shell.\n   \n   # otherwise, you can set the jdk manually with `sdk env`\n   $ sdk env\n   \n   Using java version 11.0.16-tem in this shell.\n   \n   # verify your version\n   $ java -version\n   openjdk version \"11.0.16\" 2022-07-19\n   OpenJDK Runtime Environment Temurin-11.0.16+8 (build 11.0.16+8)\n   OpenJDK 64-Bit Server VM Temurin-11.0.16+8 (build 11.0.16+8, mixed mode)\n   ```\n\n   **Hint**: [.sdkmanrc](../.sdkmanrc) defines our suggested jdk.\n\n3. Install sbt, run `sdk install sbt` or follow the official [instructions](https://www.scala-sbt.org/download.html).\n\n4. Node setup, we highly recommend [nvm](https://github.com/nvm-sh/nvm) due to its simplicity to switch between different node versions, run `nvm use` to pick the project's suggested node version, or follow [nvm-instructions](https://github.com/nvm-sh/nvm#automatically-call-nvm-use) to pick the right version automatically:\n\n   ```shell\n   # nvm can pick the right node version when moving into the project's directory\n   $ cd scala-webapp-template\n   Found '~/scala-webapp-template/.nvmrc' with version <16.7.0>\n   Now using node v16.7.0 (npm v7.20.3)\n   \n   # otherwise, you can set the node version manually with `nvm use`\n   $ nvm use\n   Found '~/scala-webapp-template/.nvmrc' with version <16.7.0>\n   Now using node v16.7.0 (npm v7.20.3)\n   \n   # verify your version\n   $ node --version\n   v16.7.0\n   ```\n\n   **Hint**: [.nvmrc](../.nvmrc) defines our suggested node version.\n\n5. Install [yarn](https://classic.yarnpkg.com/en/docs/install), most times, `npm install --global yarn` should be enough (we have tested this with yarn v1), be aware that this must installed at the node version you set in the previous step:\n\n   ```shell\n   # yarn -version\n   1.22.11\n   ```\n\nThat's it, now just run `sbt compile` to compile the project (the first time it could take several minutes).\n\n\n## Runtime dependencies\n\n### Postgres\nPostgreSQL is the only required runtime dependency, it can be installed by following the official [docs](https://www.postgresql.org/download/), it can also be run with docker.\n\nWhat matters is that you can connect to it with `psql -U postgres -h 127.0.0.1` (`postgres` is the default username, if you changed it, you must update the command too).\n\n**Hint**: We use `127.0.0.1` to force `psql` to use a TCP connection instead of a unix socket which (`localhost`), this happens because the app connects to postgres through TCP.\n\nOnce you are connected into postgres, we'll create a database for our app, and, any necessary dependencies:\n\n```postgres-sql\n-- create a database for the app\nCREATE DATABASE wiringbits_db;\n\n-- connect to it\n\\c wiringbits_db;\n\n-- create an extension used by the app\nCREATE EXTENSION IF NOT EXISTS CITEXT;\n```\n\n### AWS Email Service\nWe are using [SES](https://aws.amazon.com/ses/) to send emails (like the account verification email, password recovery, etc), what matters is to get AWS keys with access to SES.\n\nThis is an optional requirement, if you decide to ignore it, everything should work fine.\n\n### direnv\n[direnv](https://direnv.net/) is super handy to define your custom app settings without modifying any of the application files tracked by git (sorry windows). It is optional but highly recommended.\n\nIn short, it will allow you to create a `.envrc` file with all your custom settings, which will be loaded when moving into the project's directory (don't forget the [hook](https://direnv.net/docs/hook.html))\n\n### Custom config\nIt is very likely that the default settings won't work for you, at least, you will be expected to update the settings to match your postgres credentials (and SES if used).\n\nThere are two ways:\n\n1. Update [application.conf](../server/src/main/resources/application.conf)\nUpdate application.conf to set your environment specific values (just avoid committing these).\n2. Use `direnv`, create `.envrc` to export environment variables for your custom settings (don't forget to run `direnv allow` after that), get inspired by this example, it is unlikely that you will need to change any other settings:\n\n   ```shell\n   # postgres settings\n   export POSTGRES_HOST=\"127.0.0.1\"\n   export POSTGRES_DATABASE=\"wiringbits_db\"\n   export POSTGRES_USERNAME=\"postgres\"\n   export POSTGRES_PASSWORD=\"postgres\"\n   \n   # emails\n   export EMAIL_SENDER_ADDRESS=\"test@wiringbits.net\"\n   export EMAIL_PROVIDER=none\n\n   # aws, required only if the email provider is AWS\n   export AWS_REGION=\"us-west-2\"\n   export AWS_ACCESS_KEY_ID=\"REPLACE_ME\"\n   export AWS_SECRET_ACCESS_KEY=\"REPLACE_ME\"\n   ```\n\n### Run\n\nTime to run the app:\n\n1. `sbt server/run` launches the backend, which is started once you launch a request (like `curl localhost:9000`), swagger-ui available at `http://localhost:9000/docs/index.html`.\n2. `sbt dev-web` launches the main web app at `localhost:8080`\n\n\n**Hints**:\n\n- All these apps are automatically reloaded on code changes.\n- The server app prints the settings after starting, double check that they match your custom settings.\n- By default, outgoing emails are logged, be sure to check those to look for the email verification links.\n\n## Test dependencies\n\nDocker is the only required dependency to run the integration tests.\n\nEach integration test mounts its own clean database through docker, which removes the need to worry about tests polluting data.\n\nCheck the official [docs](https://docs.docker.com/engine/install/) to get it installed, it is ideal that docker can be executed without `sudo`, when this command works, tests must run too: `docker run hello-world`\n\nCommands:\n1. `sbt test` runs all the tests.\n2. `sbt server/test` runs the server tests only.\n\n**Hint** IntelliJ allows running all tests in a file, or a single test through its UI, which is very handy.\n\n\n## Deployment setup\n\nCheck the [infra](../infra/README.md) project.\n"
  },
  {
    "path": "docs/swagger-integration.md",
    "content": "# Swagger integration\n\nWe have a swagger integration so that users can explore the server API through swagger-ui.\n\nSome highlights:\n\n- We are using [tapir](https://tapir.softwaremill.com/) which integrates an [open-api](https://tapir.softwaremill.com/en/latest/docs/openapi.html) module for swagger.\n- Swagger-ui is exposed locally at [http://localhost:9000/docs](http://localhost:9000/docs).\n- Be sure to check existing [endpoints](../lib/api/shared/src/main/scala/net/wiringbits/api/endpoints) to see real examples.\n- `Option[T]` values are supported, sending a json without the key and value will be interpreted as `None`, otherwise, `Some(value)` will be sent.\n\n## Creating an endpoint definition\nWe have to define our endpoints at the [endpoints](../lib/api/shared/src/main/scala/net/wiringbits/api/endpoints) package, for example:\n\n```scala\nval basicPostEndpoint = endpoint\n  .post(\"basic\") // points to POST http://localhost:9000/basic\n  .tag(\"Misc\") // tags the endpoint as \"Misc\" on swagger-ui\n  .in(\n    jsonBody[Basic.Request].example( // expects a JSON body of type BasicGet.Request with example values\n      BasicGet.Request(\n        name = \"Alexis\",\n        email = \"alexis@wiringbits.net\"\n      )\n    )\n  )\n  .out(\n    jsonBody[Basic.Response].example( // returns a JSON body of type BasicGet.Response with example values\n      BasicGet.Response(\n        message = \"Hello Alexis!\"\n      )\n    )\n  )\n```\n\nApi models must have an `implicit Schema` defined, for example:\n\n```scala\nSchema\n  .derived[Response]\n  .name(Schema.SName(\"BasicResponse\"))\n  .description(\"Says hello to the user\")\n```\n\nAnd then integrate the endpoint to the [ApiRouter](../server/src/main/scala/controllers/ApiRouter.scala) file:\n\n```scala\nobject ApiRouter {\n  private def routes(implicit ec: ExecutionContext): List[AnyEndpoint] = List(\n    basicPostEndpoint\n  )\n}\n```\n\n## Endpoint user authentication details\n\nWe use Play Session cookie for user authentication, this is a cookie that's stored securely and is sent on every request, this cookie is used to identify the user and to check if the user is authenticated.\n\nAny endpoint that requieres user authentication must include our implicit [userAuth](../lib/api/shared/src/main/scala/net/wiringbits/api/endpoints/package.scala) handler and convert the endpoint `val` to `def` that receives an implicit handler `implicit\nauthHandler: ServerRequest => Future[UUID]`, for example:\n\n[//]: # (TODO: change Future[UUID] to Future[UserId] after mergin typo)\n```scala\ndef basicEndpoint(implicit authHandler: ServerRequest => Future[UUID]) = endpoint.get\n  .in(userAuth)\n```\n\nFor more information about creating endpoints, please check the [tapir documentation](https://tapir.softwaremill.com/en/latest/).\n"
  },
  {
    "path": "infra/.gitignore",
    "content": "apps/\n.vault\nprod-hosts.ini\nconfig/server/demo.env.j2\n.scala-build/\n\n"
  },
  {
    "path": "infra/README.md",
    "content": "# Infra\nThis project includes the necessary scripts and configuration files to deploy the applications to cloud servers (like a DigitalOcean Droplet, or an Amazon EC2 instance).\n\n## Requirements\nThe scripts work with [Ansible](https://www.ansible.com/) `2.9.23`, it is likely that other versions would work too.\n\nThere [test-hosts.ini](./test-hosts.ini) inventory file is an example configuration that is used to deploy the demo apps, it includes the necessary comments for adapting it to your own environment.\n\nA postgres database is required, you can use either a managed database or set up a local one by following these [instructions](./setup-postgres.md).\n\nModify the [server](./config/server/dev.env.j2) configuration that are required while deploying it.\n\nThe scripts are tested in Ubuntu 20.04 with paswordless sudo (meaning that `sudo ls` works without a password), they likely works in other Ubuntu based operating systems.\n\n\n## Playbooks\nThere are many playbooks involved to let you deploy the necessary pieces only:\n- [server.yml](./server.yml) deploys the [server](../server) application to the cloud server.\n- [web.yml](./web.yml) deploys the [web](../web) application to the cloud server.\n- [admin.yml](./admin.yml) deploys the [admin](../admin) application to the cloud server.\n- [nginx.yml](./nginx.yml) installs nginx in the cloud server, which is used to serve the requests from the public internet.\n- [nginx_site_admin.yml](./nginx_site_admin.yml) exposes the [admin](../admin) application to the internet, also, it gets and configures a SSL certificate to access it using https, to run this, nginx should be already deployed, also, a domain should be linked to your cloud server.\n- [nginx_site_web.yml](./nginx_site_web.yml) exposes the [web](../web) application to the internet, also, it gets and configures a SSL certificate to access it using https, to run this, nginx should be already deployed, also, a domain should be linked to your cloud server.\n\n**NOTE** You will likely run the nginx stuff only once.\n\nAfter setting up everything:\n1. Deploy nginx: `ansible-playbook -i test-hosts.ini nginx.yml`\n2. Deploy the apps with: `ansible-playbook -i test-hosts.ini server.yml web.yml admin.yml`\n3. Expose the apps to the internet with: `ansible-playbook -i test-hosts.ini nginx_site_admin.yml nginx_site_web.yml`\n\nOnce everything is ready, run the first step to deploy the apps again (or use a single playbook to deploy a single app instead).\n"
  },
  {
    "path": "infra/admin.yml",
    "content": "---\n- hosts: frontend\n  gather_facts: no\n  vars:\n    - webapp_source_zip: \"apps/admin.zip\"\n    - webapp_remote_file: \"admin.zip\"\n\n  tasks:\n    - name: Build the application\n      shell: ./scripts/build-admin.sh {{ admin_api_url }}\n      delegate_to: 127.0.0.1\n\n    - name: Install unzip\n      become: yes\n      apt:\n        name: unzip\n        state: latest\n        update_cache: yes\n\n    - name: Upload the application\n      synchronize:\n        src: \"{{ webapp_source_zip }}\"\n        dest: \"{{ webapp_remote_file }}\"\n\n    - name: Create the admin data directory\n      become: yes\n      file:\n        path: \"{{ webapp_admin_assets_directory }}\"\n        state: directory\n        owner: www-data\n        group: www-data\n\n    - name: Unpack the application\n      become: yes\n      unarchive:\n        remote_src: yes\n        src: admin.zip\n        dest: \"{{ webapp_admin_assets_directory }}\"\n\n    - name: Set the permissions\n      become: yes\n      file:\n        dest: \"{{ webapp_admin_assets_directory }}\"\n        owner: www-data\n        group: www-data\n        recurse: yes\n\n    - name: Reload nginx config\n      become: yes\n      service:\n        name: nginx\n        state: reloaded"
  },
  {
    "path": "infra/config/nginx/admin-app-htpasswd",
    "content": "demo:$apr1$8bJ.PWGf$3IxfPeFYxWRQkCw3yvRfp0"
  },
  {
    "path": "infra/config/nginx/admin_app_site.j2",
    "content": "server {\n  listen 80;\n\n  auth_basic \"Administrators only\";\n  auth_basic_user_file {{ nginx_admin_settings_file }};\n\n  server_name {{ admin_app_domain }};\n  root {{ webapp_admin_assets_directory }};\n  index index.html;\n\n  set_real_ip_from 10.0.0.0/8;\n  real_ip_header X-Real-IP;\n  real_ip_recursive on;\n  client_body_buffer_size 128k;\n  proxy_connect_timeout 60;\n  proxy_send_timeout 180s;\n  proxy_read_timeout 180s;\n  proxy_buffer_size 64k;\n  proxy_busy_buffers_size 128k;\n  proxy_buffers 64 16k;\n\n  location /api {\n    proxy_set_header  Host $host;\n    proxy_set_header  X-Real-IP $remote_addr;\n    proxy_set_header  X-Forwarded-For $remote_addr;\n    proxy_set_header  X-Forwarded-Host $remote_addr;\n    proxy_set_header  X-Forwarded-User $remote_user;\n    proxy_set_header  X-Forwarded-Proto https;\n    proxy_http_version 1.1;\n    proxy_set_header Connection \"\";\n\n    rewrite ^/api/(.*) /$1 break;\n    proxy_pass http://localhost:{{ server_app_port }};\n  }\n\n  # caching static assets\n  location ~* \\.(jpg|jpeg|png|gif|ico|css|js)$ {\n    expires 7d;\n  }\n\n  location / {\n    try_files $uri $uri/ /index.html;\n  }\n}\n"
  },
  {
    "path": "infra/config/nginx/mime.types",
    "content": "\ntypes {\n    text/html                             html htm shtml;\n    text/css                              css;\n    text/xml                              xml;\n    image/gif                             gif;\n    image/jpeg                            jpeg jpg;\n    application/javascript                js;\n    application/atom+xml                  atom;\n    application/rss+xml                   rss;\n\n    text/mathml                           mml;\n    text/plain                            txt;\n    text/vnd.sun.j2me.app-descriptor      jad;\n    text/vnd.wap.wml                      wml;\n    text/x-component                      htc;\n\n    image/png                             png;\n    image/tiff                            tif tiff;\n    image/vnd.wap.wbmp                    wbmp;\n    image/x-icon                          ico;\n    image/x-jng                           jng;\n    image/x-ms-bmp                        bmp;\n    image/svg+xml                         svg svgz;\n    image/webp                            webp;\n\n    application/font-woff                 woff;\n    application/java-archive              jar war ear;\n    application/json                      json;\n    application/mac-binhex40              hqx;\n    application/msword                    doc;\n    application/pdf                       pdf;\n    application/postscript                ps eps ai;\n    application/rtf                       rtf;\n    application/vnd.apple.mpegurl         m3u8;\n    application/vnd.ms-excel              xls;\n    application/vnd.ms-fontobject         eot;\n    application/vnd.ms-powerpoint         ppt;\n    application/vnd.wap.wmlc              wmlc;\n    application/vnd.google-earth.kml+xml  kml;\n    application/vnd.google-earth.kmz      kmz;\n    application/x-7z-compressed           7z;\n    application/x-cocoa                   cco;\n    application/x-java-archive-diff       jardiff;\n    application/x-java-jnlp-file          jnlp;\n    application/x-makeself                run;\n    application/x-perl                    pl pm;\n    application/x-pilot                   prc pdb;\n    application/x-rar-compressed          rar;\n    application/x-redhat-package-manager  rpm;\n    application/x-sea                     sea;\n    application/x-shockwave-flash         swf;\n    application/x-stuffit                 sit;\n    application/x-tcl                     tcl tk;\n    application/x-x509-ca-cert            der pem crt;\n    application/x-xpinstall               xpi;\n    application/xhtml+xml                 xhtml;\n    application/xspf+xml                  xspf;\n    application/zip                       zip;\n\n    application/octet-stream              bin exe dll;\n    application/octet-stream              deb;\n    application/octet-stream              dmg;\n    application/octet-stream              iso img;\n    application/octet-stream              msi msp msm;\n\n    application/vnd.openxmlformats-officedocument.wordprocessingml.document    docx;\n    application/vnd.openxmlformats-officedocument.spreadsheetml.sheet          xlsx;\n    application/vnd.openxmlformats-officedocument.presentationml.presentation  pptx;\n\n    audio/midi                            mid midi kar;\n    audio/mpeg                            mp3;\n    audio/ogg                             ogg;\n    audio/x-m4a                           m4a;\n    audio/x-realaudio                     ra;\n\n    video/3gpp                            3gpp 3gp;\n    video/mp2t                            ts;\n    video/mp4                             mp4;\n    video/mpeg                            mpeg mpg;\n    video/quicktime                       mov;\n    video/webm                            webm;\n    video/x-flv                           flv;\n    video/x-m4v                           m4v;\n    video/x-mng                           mng;\n    video/x-ms-asf                        asx asf;\n    video/x-ms-wmv                        wmv;\n    video/x-msvideo                       avi;\n}\n"
  },
  {
    "path": "infra/config/nginx/nginx.conf",
    "content": "user www-data;\nworker_processes auto;\npid /run/nginx.pid;\n\nevents {\n\tworker_connections 4096;\n\t# multi_accept on;\n}\n\nhttp {\n\n\t##\n\t# Basic Settings\n\t##\n\n\tsendfile on;\n\ttcp_nopush on;\n\ttcp_nodelay on;\n\tkeepalive_timeout 300s;\n\tkeepalive_requests 2000;\n\n\ttypes_hash_max_size 2048;\n\t# server_tokens off;\n\n\t# server_names_hash_bucket_size 64;\n\t# server_name_in_redirect off;\n\n\tinclude /etc/nginx/mime.types;\n\tdefault_type application/octet-stream;\n\n\t##\n\t# SSL Settings\n\t##\n\n\tssl_protocols TLSv1 TLSv1.1 TLSv1.2; # Dropping SSLv3, ref: POODLE\n\tssl_prefer_server_ciphers on;\n\n\t##\n\t# Logging Settings\n\t##\n\n\taccess_log /var/log/nginx/access.log;\n\terror_log /var/log/nginx/error.log;\n\n\t##\n\t# Gzip Settings\n\t##\n\n\tgzip on;\n\tgzip_disable \"msie6\";\n\n\tgzip_vary on;\n\tgzip_proxied any;\n\tgzip_comp_level 6;\n\tgzip_buffers 16 8k;\n\tgzip_http_version 1.1;\n\tgzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;\n\tgzip_min_length 512;\n\n\t##\n\t# Virtual Host Configs\n\t##\n\n\tinclude /etc/nginx/conf.d/*.conf;\n\tinclude /etc/nginx/sites-enabled/*;\n}\n"
  },
  {
    "path": "infra/config/nginx/preview_admin_app_site.j2",
    "content": "server {\n\n    # listen [::]:443 ssl ipv6only=on; # managed by Certbot\n    listen 443 ssl; # managed by Certbot\n    ssl_certificate /etc/letsencrypt/live/dev.wiringbits.mx/fullchain.pem; # managed by Certbot\n    ssl_certificate_key /etc/letsencrypt/live/dev.wiringbits.mx/privkey.pem; # managed by Certbot\n    include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot\n    ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot\n\n  auth_basic \"Administrators only\";\n  auth_basic_user_file {{ nginx_admin_settings_file }};\n\n  server_name {{ admin_app_domain }};\n  root {{ webapp_admin_assets_directory }};\n  index index.html;\n\n  set_real_ip_from 10.0.0.0/8;\n  real_ip_header X-Real-IP;\n  real_ip_recursive on;\n  client_body_buffer_size 128k;\n  proxy_connect_timeout 60;\n  proxy_send_timeout 180s;\n  proxy_read_timeout 180s;\n  proxy_buffer_size 64k;\n  proxy_busy_buffers_size 128k;\n  proxy_buffers 64 16k;\n\n  location /api {\n    proxy_set_header  Host $host;\n    proxy_set_header  X-Real-IP $remote_addr;\n    proxy_set_header  X-Forwarded-For $remote_addr;\n    proxy_set_header  X-Forwarded-Host $remote_addr;\n    proxy_set_header  X-Forwarded-User $remote_user;\n    proxy_set_header  X-Forwarded-Proto https;\n    proxy_http_version 1.1;\n    proxy_set_header Connection \"\";\n\n    rewrite ^/api/(.*) /$1 break;\n    proxy_pass http://localhost:{{ server_app_port }};\n  }\n\n  # caching static assets\n  location ~* \\.(jpg|jpeg|png|gif|ico|css|js)$ {\n    expires 7d;\n  }\n\n  location / {\n    try_files $uri $uri/ /index.html;\n  }\n}\n\n\nserver {\n    if ($host = {{ admin_app_domain }}) {\n        return 301 https://$host$request_uri;\n    } # managed by Certbot\n\n  listen 80;\n\n  server_name {{ admin_app_domain }};\n    return 404; # managed by Certbot\n\n}\n"
  },
  {
    "path": "infra/config/nginx/preview_web_app_site.j2",
    "content": "server {\n\n    # listen [::]:443 ssl ipv6only=on; # managed by Certbot\n    listen 443 ssl; # managed by Certbot\n    ssl_certificate /etc/letsencrypt/live/dev.wiringbits.mx/fullchain.pem; # managed by Certbot\n    ssl_certificate_key /etc/letsencrypt/live/dev.wiringbits.mx/privkey.pem; # managed by Certbot\n    include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot\n    ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot\n\n\n  server_name {{ web_app_domain }};\n  root {{ webapp_assets_directory }};\n  index index.html;\n\n  set_real_ip_from 10.0.0.0/8;\n  real_ip_header X-Real-IP;\n  real_ip_recursive on;\n  client_body_buffer_size 128k;\n  proxy_connect_timeout 60;\n  proxy_send_timeout 180s;\n  proxy_read_timeout 180s;\n  proxy_buffer_size 64k;\n  proxy_busy_buffers_size 128k;\n  proxy_buffers 64 16k;\n\n  # swagger docs\n  location /swagger.json {\n    proxy_pass http://localhost:{{ server_app_port }};\n  }\n\n  # the admin api is only reachable when providing the necessary credentials\n  location /api/admin {\n    auth_basic \"Administrators only\";\n    auth_basic_user_file {{ nginx_admin_settings_file }};\n\n    proxy_set_header  Host $host;\n    proxy_set_header  X-Real-IP $remote_addr;\n    proxy_set_header  X-Forwarded-For $remote_addr;\n    proxy_set_header  X-Forwarded-Host $remote_addr;\n    proxy_set_header  X-Forwarded-User $remote_user;\n    proxy_set_header  X-Forwarded-Proto https;\n    proxy_http_version 1.1;\n    proxy_set_header Connection \"\";\n\n    rewrite ^/api/(.*) /$1 break;\n    proxy_pass http://localhost:{{ server_app_port }};\n  }\n\n  location /api {\n    proxy_set_header  Host $host;\n    proxy_set_header  X-Real-IP $remote_addr;\n    proxy_set_header  X-Forwarded-For $remote_addr;\n    proxy_set_header  X-Forwarded-Host $remote_addr;\n    proxy_set_header  X-Forwarded-User $remote_user;\n    proxy_set_header  X-Forwarded-Proto https;\n    proxy_http_version 1.1;\n    proxy_set_header Connection \"\";\n\n    rewrite ^/api/(.*) /$1 break;\n    proxy_pass http://localhost:{{ server_app_port }};\n  }\n\n  # FIXME: This prevents returning the static resources from the app, like /api/swagger.json\n  # caching static assets\n  # location ~* \\.(jpg|jpeg|png|gif|ico|css|js)$ {\n  #   expires 7d;\n  # }\n\n  location / {\n    try_files $uri $uri/ /index.html;\n  }\n}\n\nserver {\n    if ($host = {{ web_app_domain }}) {\n        return 301 https://$host$request_uri;\n    } # managed by Certbot\n\n  listen 80;\n\n  server_name {{ web_app_domain }};\n    return 404; # managed by Certbot\n\n}\n"
  },
  {
    "path": "infra/config/nginx/web_app_site.j2",
    "content": "server {\n  listen 80;\n\n  server_name {{ web_app_domain }};\n  root {{ webapp_assets_directory }};\n  index index.html;\n\n  set_real_ip_from 10.0.0.0/8;\n  real_ip_header X-Real-IP;\n  real_ip_recursive on;\n  client_body_buffer_size 128k;\n  proxy_connect_timeout 60;\n  proxy_send_timeout 180s;\n  proxy_read_timeout 180s;\n  proxy_buffer_size 64k;\n  proxy_busy_buffers_size 128k;\n  proxy_buffers 64 16k;\n\n  # swagger docs\n  location /swagger.json {\n    proxy_pass http://localhost:{{ server_app_port }};\n  }\n\n  # the admin api is only reachable when providing the necessary credentials\n  location /api/admin {\n    auth_basic \"Administrators only\";\n    auth_basic_user_file {{ nginx_admin_settings_file }};\n\n    proxy_set_header  Host $host;\n    proxy_set_header  X-Real-IP $remote_addr;\n    proxy_set_header  X-Forwarded-For $remote_addr;\n    proxy_set_header  X-Forwarded-Host $remote_addr;\n    proxy_set_header  X-Forwarded-User $remote_user;\n    proxy_set_header  X-Forwarded-Proto https;\n    proxy_http_version 1.1;\n    proxy_set_header Connection \"\";\n\n    rewrite ^/api/(.*) /$1 break;\n    proxy_pass http://localhost:{{ server_app_port }};\n  }\n\n  location /api {\n    proxy_set_header  Host $host;\n    proxy_set_header  X-Real-IP $remote_addr;\n    proxy_set_header  X-Forwarded-For $remote_addr;\n    proxy_set_header  X-Forwarded-Host $remote_addr;\n    proxy_set_header  X-Forwarded-User $remote_user;\n    proxy_set_header  X-Forwarded-Proto https;\n    proxy_http_version 1.1;\n    proxy_set_header Connection \"\";\n\n    rewrite ^/api/(.*) /$1 break;\n    proxy_pass http://localhost:{{ server_app_port }};\n  }\n\n  # FIXME: This prevents returning the static resources from the app, like /api/swagger.json\n  # caching static assets\n  # location ~* \\.(jpg|jpeg|png|gif|ico|css|js)$ {\n  #   expires 7d;\n  # }\n\n  location / {\n    try_files $uri $uri/ /index.html;\n  }\n}\n"
  },
  {
    "path": "infra/config/server/dev.env.j2",
    "content": "POSTGRES_DATABASE=\"server_db\"\nPOSTGRES_USERNAME=\"db_user\"\nPOSTGRES_PASSWORD=\"REPLACE_ME\"\nPOSTGRES_HOST=\"127.0.0.1\"\n\nPLAY_APPLICATION_SECRET=\"REPLACE_ME\"\nJWT_SECRET=\"REPLACE_ME\"\nJWT_ENFORCED=false\n\nPLAY_SESSION_SECURE=true\nPLAY_SESSION_DOMAIN=\"{{ web_app_domain }}\"\n\nAPP_ALLOWED_HOST_1=\"{{ web_app_domain }}\"\nAPP_ALLOWED_HOST_2=\"{{ admin_app_domain }}\"\n\nAWS_REGION=\"us-east-1\"\nAWS_ACCESS_KEY_ID=\"REPLACE_ME\"\nAWS_SECRET_ACCESS_KEY=\"REPLACE_ME\"\n\nEMAIL_SENDER_ADDRESS=\"template@wiringbits.net\"\n\nWEBAPP_HOST=\"https://template-demo.wiringbits.net\"\n\nUSER_TOKENS_HMAC_SECRET=\"REPLACE_ME\"\n"
  },
  {
    "path": "infra/config/server/server.service.j2",
    "content": "[Unit]\nDescription={{ app_systemd_service_name }}\n\n[Service]\nType=simple\nWorkingDirectory={{ app_home }}/{{ app_directory_name }}\nStandardOutput=tty\nStandardError=tty\nEnvironmentFile={{ app_env_config_file }}\nLimitNOFILE=65535\nUser={{ app_user }}\nExecStart={{ app_home }}/{{ app_directory_name }}/bin/{{ app_startup_script }} -Dpidfile.path=/dev/null -Dhttp.port={{ server_app_port }}\nRestart=on-failure\n\n[Install]\nWantedBy=multi-user.target\n"
  },
  {
    "path": "infra/demo-hosts.ini",
    "content": "[webapp]\n[webapp:vars]\nansible_user=ubuntu\nansible_ssh_extra_args='-o StrictHostKeyChecking=no'\n\nserver_app_port=9000\n\n# the domain where the main app is going to run\n# you are expected to have already created a DNS \"A\" record pointing to the server that will host the app\nweb_app_domain=\"template-demo.wiringbits.net\"\n\n# the domain where the admin app is going to run\n# you are expected to have already created a DNS \"A\" record pointing to the server that will host the app\nadmin_app_domain=\"template-demo-admin.wiringbits.net\"\n\n[webapp:children]\nbackend\nfrontend\n\n\n[backend]\nbackend-server ansible_host=64.227.100.33\n[backend:vars]\n# this is where the environment variables required by the app are defined\n# it could be kept encrypted by using ansible-vault\napp_env_config_source=config/server/demo.env.j2\n\n# defines the systemd service used to run the app\napp_systemd_service_source=config/server/server.service.j2\n\n# the service name used to register the service in systemd, for example,\n# restarting the app would be done by invoking: service app-server restart\napp_systemd_service_name=\"wiringbits-server\"\n\n# the directory name where the app is stored after building it\n# this depends on your app name, get it by running \"sbt server/dist\" in the app's source\n# the last logs will display a line like:\n# [info] Your package is ready in .../server/target/universal/wiringbits-server-0.1.0-SNAPSHOT.zip\n# the last part is the source name, remove the zip extension and that's the directory name,\n# remove the version from the directory name and that's the startup script\napp_source_name=\"wiringbits-server-0.1.0-SNAPSHOT.zip\"\napp_directory_name=\"wiringbits-server-0.1.0-SNAPSHOT\"\napp_startup_script=\"wiringbits-server\"\n\n# user/group/home used to store the app\napp_user=\"play\"\napp_group=\"play\"\napp_home=\"/home/play/app\"\napp_env_config_file=\"/home/play/app/.env\"\n\n[frontend]\nfrontend-server ansible_host=64.227.100.33\n[frontend:vars]\n# this is necessary to get SSL certificates, it is the email for receiving notifications from letsencrypt\nletsencrypt_notifications_email=certbot@wiringbits.net\n\n# the url where the server/backend api is exposed\n# this depends on the nginx settings\nweb_api_url=\"https://template-demo.wiringbits.net/api\"\n\n# the url where the server/backend api is exposed (for the admin website)\n# this depends on the nginx settings\nadmin_api_url=\"https://template-demo-admin.wiringbits.net/api\"\n\n# the settings to enable http basic authorization with nginx while accessing the admin app\n# it can be generated by running  `htpasswd`, for defining user called \"demo\" this could be run:\n# - htpasswd -n demo > config/nginx/admin-app-htpasswd\nnginx_admin_password_file=config/nginx/admin-app-htpasswd\n\nwebapp_assets_directory=/var/www/html\nwebapp_admin_assets_directory=/var/www/admin\n"
  },
  {
    "path": "infra/nginx.yml",
    "content": "---\n- hosts: frontend\n  gather_facts: no\n  vars:\n    - nginx_config_file: \"config/nginx/nginx.conf\"\n    - nginx_mime_types_file: \"config/nginx/mime.types\"\n\n  tasks:\n    - name: Install nginx\n      become: yes\n      apt:\n        name: nginx\n        state: latest\n        update_cache: yes\n\n    - name: Disable nginx default site\n      become: yes\n      file:\n        path: /etc/nginx/sites-enabled/default\n        state: absent\n\n    - name: Copy the nginx config\n      become: yes\n      copy:\n        src: \"{{ nginx_config_file }}\"\n        dest: /etc/nginx/nginx.conf\n\n    - name: Copy mime.types\n      become: yes\n      copy:\n        src: \"{{ nginx_mime_types_file }}\"\n        dest: /etc/nginx/mime.types\n\n    - name: Restart nginx\n      become: yes\n      service:\n        name: nginx\n        state: restarted\n\n    - name: Enable nginx to run on system startup\n      become: yes\n      systemd:\n        name: nginx\n        enabled: yes\n"
  },
  {
    "path": "infra/nginx_site_admin.yml",
    "content": "---\n- hosts: frontend\n  gather_facts: no\n  vars:\n    - nginx_site_file: \"config/nginx/admin_app_site.j2\"\n    - nginx_admin_settings_directory: \"/etc/nginx/admin-config\"\n    - nginx_admin_settings_file: \"/etc/nginx/admin-config/htpasswd\"\n\n  tasks:\n    - name: Create the custom config directory\n      become: yes\n      file:\n        path: \"{{ nginx_admin_settings_directory }}\"\n        state: directory\n\n    # file generated with `htpasswd -n demo`, user = demo, pass = wiringbits\n    - name: Copy the site password\n      become: yes\n      copy:\n        src: \"{{ nginx_admin_password_file }}\"\n        dest: \"{{ nginx_admin_settings_file }}\"\n\n    - name: Create the sites-available directory\n      become: yes\n      file:\n        path: /etc/nginx/sites-available\n        state: directory\n\n    - name: Copy the site config\n      become: yes\n      template:\n        src: \"{{ nginx_site_file }}\"\n        dest: /etc/nginx/sites-available/{{ admin_app_domain }}\n\n    - name: Create the sites-enabled directory\n      become: yes\n      file:\n        path: /etc/nginx/sites-enabled\n        state: directory\n\n    - name: Enable the site\n      become: yes\n      file:\n        src: /etc/nginx/sites-available/{{ admin_app_domain }}\n        dest: /etc/nginx/sites-enabled/{{ admin_app_domain }}\n        state: link\n\n    - name: Install snapd\n      become: yes\n      apt:\n        name: snapd\n        state: latest\n        update_cache: yes\n\n    # Get SSL certificate\n    # Source: https://certbot.eff.org/lets-encrypt/ubuntufocal-nginx\n    - name: Install certbot\n      become: yes\n      snap:\n        name: certbot\n        classic: yes\n\n    - name: Get SSL certificate\n      become: yes\n      shell: certbot -n --agree-tos --nginx -m \"{{ letsencrypt_notifications_email }}\" --domains \"{{ admin_app_domain }}\"\n"
  },
  {
    "path": "infra/nginx_site_web.yml",
    "content": "---\n- hosts: frontend\n  gather_facts: no\n  vars:\n    - nginx_site_file: \"config/nginx/web_app_site.j2\"\n    - nginx_admin_settings_file: \"/etc/nginx/admin-config/htpasswd\"\n\n  tasks:\n    - name: Create the sites-available directory\n      become: yes\n      file:\n        path: /etc/nginx/sites-available\n        state: directory\n\n    - name: Copy the site config\n      become: yes\n      template:\n        src: \"{{ nginx_site_file }}\"\n        dest: /etc/nginx/sites-available/{{ web_app_domain }}\n\n    - name: Create the sites-enabled directory\n      become: yes\n      file:\n        path: /etc/nginx/sites-enabled\n        state: directory\n\n    - name: Enable the site\n      become: yes\n      file:\n        src: /etc/nginx/sites-available/{{ web_app_domain }}\n        dest: /etc/nginx/sites-enabled/{{ web_app_domain }}\n        state: link\n\n    - name: Install snapd\n      become: yes\n      apt:\n        name: snapd\n        state: latest\n        update_cache: yes\n\n    # Get SSL certificate\n    # Source: https://certbot.eff.org/lets-encrypt/ubuntufocal-nginx\n    - name: Install certbot\n      become: yes\n      snap:\n        name: certbot\n        classic: yes\n\n    - name: Get SSL certificate\n      become: yes\n      shell: certbot -n --agree-tos --nginx -m \"{{ letsencrypt_notifications_email }}\" --domains \"{{ web_app_domain }}\"\n"
  },
  {
    "path": "infra/preview_nginx_site_admin.yml",
    "content": "---\n- hosts: frontend\n  gather_facts: no\n  vars:\n    - nginx_site_file: \"config/nginx/preview_admin_app_site.j2\"\n    - nginx_admin_settings_directory: \"/etc/nginx/admin-config\"\n    - nginx_admin_settings_file: \"/etc/nginx/admin-config/htpasswd\"\n\n  tasks:\n    - name: Create the custom config directory\n      become: yes\n      file:\n        path: \"{{ nginx_admin_settings_directory }}\"\n        state: directory\n\n    # file generated with `htpasswd -n demo`, user = demo, pass = wiringbits\n    - name: Copy the site password\n      become: yes\n      copy:\n        src: \"{{ nginx_admin_password_file }}\"\n        dest: \"{{ nginx_admin_settings_file }}\"\n\n    - name: Create the sites-available directory\n      become: yes\n      file:\n        path: /etc/nginx/sites-available\n        state: directory\n\n    - name: Copy the site config\n      become: yes\n      template:\n        src: \"{{ nginx_site_file }}\"\n        dest: /etc/nginx/sites-available/{{ admin_app_domain }}\n\n    - name: Create the sites-enabled directory\n      become: yes\n      file:\n        path: /etc/nginx/sites-enabled\n        state: directory\n\n    - name: Enable the site\n      become: yes\n      file:\n        src: /etc/nginx/sites-available/{{ admin_app_domain }}\n        dest: /etc/nginx/sites-enabled/{{ admin_app_domain }}\n        state: link\n\n    - name: Reload nginx config\n      become: yes\n      service:\n        name: nginx\n        state: reloaded\n"
  },
  {
    "path": "infra/preview_nginx_site_web.yml",
    "content": "---\n- hosts: frontend\n  gather_facts: no\n  vars:\n    - nginx_site_file: \"config/nginx/preview_web_app_site.j2\"\n    - nginx_admin_settings_file: \"/etc/nginx/admin-config/htpasswd\"\n\n  tasks:\n    - name: Create the sites-available directory\n      become: yes\n      file:\n        path: /etc/nginx/sites-available\n        state: directory\n\n    - name: Copy the site config\n      become: yes\n      template:\n        src: \"{{ nginx_site_file }}\"\n        dest: /etc/nginx/sites-available/{{ web_app_domain }}\n\n    - name: Create the sites-enabled directory\n      become: yes\n      file:\n        path: /etc/nginx/sites-enabled\n        state: directory\n\n    - name: Enable the site\n      become: yes\n      file:\n        src: /etc/nginx/sites-available/{{ web_app_domain }}\n        dest: /etc/nginx/sites-enabled/{{ web_app_domain }}\n        state: link\n\n    - name: Reload nginx config\n      become: yes\n      service:\n        name: nginx\n        state: reloaded\n"
  },
  {
    "path": "infra/scripts/build-admin.sh",
    "content": "#!/bin/bash\nset -e\nAPI_URL=$1\necho \"API_URL=$API_URL\" \\\n  && cd ../ \\\n  && API_URL=$API_URL sbt admin/build \\\n  && cd -\ncd ../admin/build && zip -r admin.zip * && cd -\nmkdir -p apps && mv ../admin/build/admin.zip apps/admin.zip\n"
  },
  {
    "path": "infra/scripts/build-server.sh",
    "content": "#!/bin/bash\nset -e\nAPP_SOURCE_ZIP=$1\necho \"APP_SOURCE_ZIP=$APP_SOURCE_ZIP\"\ncd ../ && SWAGGER_API_BASEPATH=\"/api\" sbt server/dist && cd -\nmkdir -p apps && cp ../server/target/universal/$APP_SOURCE_ZIP apps/server.zip\n"
  },
  {
    "path": "infra/scripts/build-web.sh",
    "content": "#!/bin/bash\nset -e\nAPI_URL=$1\necho \"API_URL=$API_URL\" \\\n  && cd ../ \\\n  && API_URL=$API_URL sbt web/build \\\n  && cd -\ncd ../web/build && zip -r web.zip * && cd -\nmkdir -p apps && mv ../web/build/web.zip apps/web.zip\n"
  },
  {
    "path": "infra/server.yml",
    "content": "---\n- hosts: backend\n  gather_facts: no\n  vars:\n    - app_source_zip: \"apps/server.zip\"\n    - app_source_zip_remote: \"server.zip\"\n\n  tasks:\n    - name: Install java11\n      become: yes\n      apt:\n        name: default-jre\n        state: latest\n\n    - name: Install unzip\n      become: yes\n      apt:\n        name: unzip\n        state: latest\n\n    - name: Build the application\n      retries: 10\n      delay: 5\n      shell: \"./scripts/build-server.sh {{ app_source_name }}\"\n      delegate_to: 127.0.0.1\n\n    - name: Upload the application\n      synchronize:\n        src: \"{{ app_source_zip }}\"\n        dest: \"{{ app_source_zip_remote }}\"\n\n    # Registering the service before unpacking the app allows to make sure the app is stopped\n    # on the first deployment.\n    - name: Add the systemd service\n      become: yes\n      template:\n        src: \"{{ app_systemd_service_source }}\"\n        dest: \"/etc/systemd/system/{{ app_systemd_service_name }}.service\"\n\n    - name: Pick up systemd changes\n      become: yes\n      systemd:\n        daemon_reload: yes\n\n    - name: Make sure the application is stopped\n      become: yes\n      systemd:\n        name: \"{{ app_systemd_service_name }}\"\n        state: stopped\n\n    # This is crucial to avoid polluting the classpath with old jars after upgrading dependencies\n    - name: Delete old application (important!)\n      become: yes\n      file:\n        path: \"{{ app_home }}/{{ app_directory_name }}\"\n        state: absent\n\n    - name: Create the app group\n      become: yes\n      group:\n        name: \"{{ app_group }}\"\n        state: present\n\n    - name: Create the app user\n      become: yes\n      user:\n        name: \"{{ app_user }}\"\n        group: \"{{ app_group }}\"\n        state: present\n        system: yes\n\n    - name: Create the app directory\n      become: yes\n      file:\n        path: \"{{ app_home }}\"\n        state: directory\n        owner: \"{{ app_user }}\"\n        group: \"{{ app_group }}\"\n\n    - name: Unpack the application\n      become: yes\n      unarchive:\n        remote_src: yes\n        src: \"{{ app_source_zip_remote }}\"\n        dest: \"{{ app_home }}\"\n        owner: \"{{ app_user }}\"\n        group: \"{{ app_group }}\"\n\n    - name: Set the application config\n      become: yes\n      template:\n        src: \"{{ app_env_config_source }}\"\n        dest: \"{{ app_env_config_file }}\"\n        owner: \"{{ app_user }}\"\n        group: \"{{ app_group }}\"\n\n    - name: Set the application files permissions\n      become: yes\n      file:\n        dest: \"{{ app_home }}\"\n        owner: \"{{ app_user }}\"\n        group: \"{{ app_group }}\"\n        recurse: yes\n\n    - name: Make sure the application is started\n      become: yes\n      systemd:\n        name: \"{{ app_systemd_service_name }}\"\n        state: started\n\n    - name: Enable the application to run on system startup\n      become: yes\n      systemd:\n        name: \"{{ app_systemd_service_name }}\"\n        enabled: yes\n\n# TODO: Check service is healthy by querying localhost:9000/health\n"
  },
  {
    "path": "infra/setup-postgres.md",
    "content": "# Setup postgres\nThis is the manual way to set up postgres for a test server, for production it is recommended to use a managed database instead.\n\nEither follow these [instructions](https://postgreshelp.com/postgresql-13-install-in-ubuntu/) or just run these commands:\n\n```bash\n# Create the file repository configuration\nsudo sh -c 'echo \"deb http://apt.postgresql.org/pub/repos/apt $(lsb_release -cs)-pgdg main\" > /etc/apt/sources.list.d/pgdg.list'\n\n# Import the repository signing key\nwget --quiet -O - https://www.postgresql.org/media/keys/ACCC4CF8.asc | sudo apt-key add -\n\n# Update the package lists\nsudo apt-get update\n\n# Install the latest version of PostgreSQL.\n# If you want a specific version, use 'postgresql-12' or similar instead of 'postgresql':\nsudo apt-get -y install postgresql-15\n```\n\nThen, connect to the database (`sudo -u postgres psql`) and create the necessary user/database:\n\n```shell\nCREATE DATABASE server_db;\n\\c server_db;\nCREATE EXTENSION IF NOT EXISTS CITEXT;\nALTER DATABASE server_db SET statement_timeout = '60s';\nALTER DATABASE server_db SET idle_in_transaction_session_timeout TO '5min';\nCREATE USER db_user WITH SUPERUSER PASSWORD 'useYourOwnPasswordInstead';\nGRANT ALL PRIVILEGES ON DATABASE \"server_db\" to db_user;\n```\n\nTest the connection to the new database with the custom user: `psql -h 127.0.0.1 -U db_user server_db`\n\nThat's it!\n"
  },
  {
    "path": "infra/test-hosts.ini",
    "content": "[webapp]\n[webapp:vars]\nansible_user=ubuntu\nansible_ssh_extra_args='-o StrictHostKeyChecking=no'\n\nserver_app_port=9000\n\n# the domain where the main app is going to run\n# you are expected to have already created a DNS \"A\" record pointing to the server that will host the app\nweb_app_domain=\"template-demo.wiringbits.net\"\n\n# the domain where the admin app is going to run\n# you are expected to have already created a DNS \"A\" record pointing to the server that will host the app\nadmin_app_domain=\"template-demo-admin.wiringbits.net\"\n\n[webapp:children]\nbackend\nfrontend\n\n\n[backend]\nbackend-server ansible_host=64.227.100.33\n[backend:vars]\n# this is where the environment variables required by the app are defined\n# it could be kept encrypted by using ansible-vault\napp_env_config_source=config/server/dev.env.j2\n\n# defines the systemd service used to run the app\napp_systemd_service_source=config/server/server.service.j2\n\n# the service name used to register the service in systemd, for example,\n# restarting the app would be done by invoking: service app-server restart\napp_systemd_service_name=\"wiringbits-server\"\n\n# the directory name where the app is stored after building it\n# this depends on your app name, get it by running \"sbt server/dist\" in the app's source\n# the last logs will display a line like:\n# [info] Your package is ready in .../server/target/universal/wiringbits-server-0.1.0-SNAPSHOT.zip\n# the last part is the source name, remove the zip extension and that's the directory name,\n# remove the version from the directory name and that's the startup script\napp_source_name=\"wiringbits-server-0.1.0-SNAPSHOT.zip\"\napp_directory_name=\"wiringbits-server-0.1.0-SNAPSHOT\"\napp_startup_script=\"wiringbits-server\"\n\n# user/group/home used to store the app\napp_user=\"play\"\napp_group=\"play\"\napp_home=\"/home/play/app\"\napp_env_config_file=\"/home/play/app/.env\"\n\n[frontend]\nfrontend-server ansible_host=64.227.100.33\n[frontend:vars]\n# this is necessary to get SSL certificates, it is the email for receiving notifications from letsencrypt\nletsencrypt_notifications_email=certbot@wiringbits.net\n\n# the url where the server/backend api is exposed\n# this depends on the nginx settings\nweb_api_url=\"https://template-demo.wiringbits.net/api\"\n\n# the url where the server/backend api is exposed (for the admin website)\n# this depends on the nginx settings\nadmin_api_url=\"https://template-demo-admin.wiringbits.net/api\"\n\n# the settings to enable http basic authorization with nginx while accessing the admin app\n# it can be generated by running  `htpasswd`, for defining user called \"demo\" this could be run:\n# - htpasswd -n demo > config/nginx/admin-app-htpasswd\nnginx_admin_password_file=config/nginx/admin-app-htpasswd\n\nwebapp_assets_directory=/var/www/html\nwebapp_admin_assets_directory=/var/www/admin\n"
  },
  {
    "path": "infra/web.yml",
    "content": "---\n- hosts: frontend\n  gather_facts: no\n  vars:\n    - webapp_source_zip: \"apps/web.zip\"\n    - webapp_remote_file: \"web.zip\"\n\n  tasks:\n    - name: Build the application\n      shell: ./scripts/build-web.sh {{ web_api_url }}\n      delegate_to: 127.0.0.1\n\n    - name: Install unzip\n      become: yes\n      apt:\n        name: unzip\n        state: latest\n        update_cache: yes\n\n    - name: Upload the application\n      synchronize:\n        src: \"{{ webapp_source_zip }}\"\n        dest: \"{{ webapp_remote_file }}\"\n\n    - name: Create the web data directory\n      become: yes\n      file:\n        path: \"{{ webapp_assets_directory }}\"\n        state: directory\n        owner: www-data\n        group: www-data\n\n    - name: Unpack the application\n      become: yes\n      unarchive:\n        remote_src: yes\n        src: \"{{ webapp_remote_file }}\"\n        dest: \"{{ webapp_assets_directory }}\"\n\n    - name: Set the permissions\n      become: yes\n      file:\n        dest: \"{{ webapp_assets_directory }}\"\n        owner: www-data\n        group: www-data\n        recurse: yes\n\n    - name: Reload nginx config\n      become: yes\n      service:\n        name: nginx\n        state: reloaded\n"
  },
  {
    "path": "lib/api/shared/src/main/scala/net/wiringbits/api/ApiClient.scala",
    "content": "package net.wiringbits.api\n\nimport net.wiringbits.api.endpoints.*\nimport net.wiringbits.api.models.*\nimport net.wiringbits.api.models.admin.{AdminGetUserLogs, AdminGetUsers}\nimport net.wiringbits.api.models.auth.{GetCurrentUser, Login, Logout}\nimport net.wiringbits.api.models.environmentconfig.GetEnvironmentConfig\nimport net.wiringbits.api.models.users.*\nimport play.api.libs.json.{Json, Reads}\nimport sttp.client3.*\nimport sttp.tapir.PublicEndpoint\nimport sttp.tapir.client.sttp.SttpClientInterpreter\nimport sttp.tapir.model.ServerRequest\n\nimport java.util.UUID\nimport scala.concurrent.{ExecutionContext, Future}\nimport scala.util.{Failure, Success, Try}\n\nobject ApiClient {\n  case class Config(serverUrl: String)\n}\n\nclass ApiClient(config: ApiClient.Config)(implicit\n    ex: ExecutionContext,\n    sttpBackend: SttpBackend[Future, _]\n) {\n  // While the server requires a userId, it is extracted from the Session cookie, we need a dummy value just to\n  // fulfill the method signatures\n  private val dummyUserId = Future.successful(UUID.fromString(\"887a5d77-cb5d-4d9c-b4dc-539c8aae3977\"))\n\n  // Similarly to the dummy userId, we need a way to derive the userId from a request, which is used only on the\n  // server-side code, this function is helpful to fulfill the method signatures\n  private implicit val handleDummyUserId: ServerRequest => Future[UUID] = _ => dummyUserId\n\n  private def asJson[R: Reads](strBody: String) = {\n    Try {\n      Json.parse(strBody).as[ErrorResponse]\n    } match {\n      case Success(error) => throw new RuntimeException(error.error)\n      case Failure(_) =>\n        Try {\n          Json.parse(strBody).as[R]\n        } match {\n          case Success(response) => response\n          case Failure(error) => throw new RuntimeException(s\"Unexpected response ${error.getMessage}\")\n        }\n    }\n  }\n\n  private val ServerAPI = sttp.model.Uri\n    .parse(config.serverUrl)\n    .getOrElse(throw new RuntimeException(\"Invalid server url\"))\n\n  private val client = SttpClientInterpreter()\n\n  /** This is necessary for non-browser clients, this way, the cookies from the last authentication response are\n    * propagated to the next requests\n    */\n  private var lastAuthResponse = Option.empty[Response[_]]\n\n  private def unsafeSetLoginResponse(response: Response[_]): Unit = synchronized {\n    lastAuthResponse = Some(response)\n  }\n\n  private def unsafeRemoveLoginResponse(): Unit = synchronized {\n    lastAuthResponse = None\n  }\n\n  private def handleRequest[I, O](endpoint: PublicEndpoint[I, ErrorResponse, O, Any], request: I): Future[O] = {\n    val savedCookies = lastAuthResponse.map(_.unsafeCookies).getOrElse(Seq.empty)\n\n    client\n      .toRequestThrowDecodeFailures(endpoint, Some(ServerAPI))\n      .apply(request)\n      .cookies(savedCookies)\n      .send(sttpBackend)\n      .map(_.body)\n      .map {\n        case Left(error) => throw new RuntimeException(error.error)\n        case Right(response) => response\n      }\n  }\n\n  def createUser(request: CreateUser.Request): Future[CreateUser.Response] =\n    handleRequest(UsersEndpoints.create, request)\n\n  def verifyEmail(request: VerifyEmail.Request): Future[VerifyEmail.Response] =\n    handleRequest(UsersEndpoints.verifyEmail, request)\n\n  def forgotPassword(request: ForgotPassword.Request): Future[ForgotPassword.Response] =\n    handleRequest(UsersEndpoints.forgotPassword, request)\n\n  def resetPassword(request: ResetPassword.Request): Future[ResetPassword.Response] =\n    handleRequest(UsersEndpoints.resetPassword, request)\n\n  def currentUser: Future[GetCurrentUser.Response] =\n    handleRequest(AuthEndpoints.getCurrentUser, dummyUserId)\n\n  def updateUser(request: UpdateUser.Request): Future[UpdateUser.Response] =\n    handleRequest(UsersEndpoints.update, (request, dummyUserId))\n\n  def updatePassword(request: UpdatePassword.Request): Future[UpdatePassword.Response] =\n    handleRequest(UsersEndpoints.updatePassword, (request, dummyUserId))\n\n  def getUserLogs: Future[GetUserLogs.Response] =\n    handleRequest(UsersEndpoints.getLogs, dummyUserId)\n\n  def adminGetUserLogs(userId: UUID): Future[AdminGetUserLogs.Response] =\n    handleRequest(AdminEndpoints.getUserLogsEndpoint, (\"_\", userId, \"\"))\n\n  def adminGetUsers: Future[AdminGetUsers.Response] =\n    handleRequest(AdminEndpoints.getUsersEndpoint, (\"_\", \"\"))\n\n  def getEnvironmentConfig: Future[GetEnvironmentConfig.Response] =\n    handleRequest(EnvironmentConfigEndpoints.getEnvironmentConfig, ())\n\n  def sendEmailVerificationToken(\n      request: SendEmailVerificationToken.Request\n  ): Future[SendEmailVerificationToken.Response] =\n    handleRequest(UsersEndpoints.sendEmailVerificationToken, request)\n\n  // login and logout are special cases, since they return a cookie, sttp-client can not decode them correctly, so we have\n  // to do it manually\n  def login(request: Login.Request): Future[Login.Response] =\n    client\n      .toRequestThrowDecodeFailures(AuthEndpoints.login, Some(ServerAPI))\n      .apply(request)\n      .response(asStringAlways)\n      .send(sttpBackend)\n      .map { response =>\n        unsafeSetLoginResponse(response)\n        response.body\n      }\n      .map(asJson[Login.Response])\n\n  def logout: Future[Logout.Response] =\n    client\n      .toRequestThrowDecodeFailures(AuthEndpoints.logout, Some(ServerAPI))\n      .apply(dummyUserId)\n      .response(asStringAlways)\n      .send(sttpBackend)\n      .map { response =>\n        unsafeRemoveLoginResponse()\n        response.body\n      }\n      .map(asJson[Logout.Response])\n}\n"
  },
  {
    "path": "lib/api/shared/src/main/scala/net/wiringbits/api/endpoints/AdminEndpoints.scala",
    "content": "package net.wiringbits.api.endpoints\n\nimport net.wiringbits.api.models\nimport net.wiringbits.api.models.admin.{AdminGetUserLogs, AdminGetUsers}\nimport net.wiringbits.api.models.ErrorResponse\nimport net.wiringbits.common.models.{Email, Name}\nimport sttp.tapir.*\nimport sttp.tapir.json.play.*\n\nimport java.time.Instant\nimport java.util.UUID\n\nobject AdminEndpoints {\n  private val baseEndpoint = endpoint\n    .in(\"admin\")\n    .tag(\"Admin\")\n    .in(adminAuth)\n    .errorOut(errorResponseErrorOut)\n\n  val getUserLogsEndpoint: Endpoint[Unit, (String, UUID, String), ErrorResponse, AdminGetUserLogs.Response, Any] =\n    baseEndpoint.get\n      .in(\"users\" / path[UUID](\"userId\") / \"logs\")\n      .in(adminHeader)\n      .out(\n        jsonBody[AdminGetUserLogs.Response].example(\n          AdminGetUserLogs.Response(\n            List(\n              AdminGetUserLogs.Response\n                .UserLog(\n                  userLogId = UUID.fromString(\"3fa85f64-5717-4562-b3fc-2c963f66afa6\"),\n                  message = \"Message\",\n                  createdAt = Instant.parse(\"2021-01-01T00:00:00Z\")\n                )\n            )\n          )\n        )\n      )\n      .errorOut(oneOf(HttpErrors.badRequest, HttpErrors.unauthorized))\n      .summary(\"Get the logs for a specific user\")\n\n  val getUsersEndpoint: Endpoint[Unit, (String, String), ErrorResponse, AdminGetUsers.Response, Any] =\n    baseEndpoint.get\n      .in(\"users\")\n      .in(adminHeader)\n      .out(\n        jsonBody[AdminGetUsers.Response].example(\n          AdminGetUsers.Response(\n            List(\n              AdminGetUsers.Response.User(\n                id = UUID.fromString(\"3fa85f64-5717-4562-b3fc-2c963f66afa6\"),\n                name = Name.trusted(\"Alexis\"),\n                email = Email.trusted(\"alexis@wiringbits.net\"),\n                createdAt = Instant.parse(\"2021-01-01T00:00:00Z\")\n              )\n            )\n          )\n        )\n      )\n      .errorOut(oneOf(HttpErrors.badRequest, HttpErrors.unauthorized))\n      .summary(\"Get the registered users\")\n\n  val routes: List[AnyEndpoint] = List(\n    getUserLogsEndpoint,\n    getUsersEndpoint\n  )\n}\n"
  },
  {
    "path": "lib/api/shared/src/main/scala/net/wiringbits/api/endpoints/AuthEndpoints.scala",
    "content": "package net.wiringbits.api.endpoints\n\nimport net.wiringbits.api.models\nimport net.wiringbits.api.models.auth.{GetCurrentUser, Login, Logout}\nimport net.wiringbits.api.models.ErrorResponse\nimport net.wiringbits.common.models.{Captcha, Email, Name, Password}\nimport sttp.tapir.*\nimport sttp.tapir.json.play.*\nimport sttp.tapir.model.ServerRequest\n\nimport java.time.Instant\nimport java.util.UUID\nimport scala.concurrent.Future\n\nobject AuthEndpoints {\n  private val baseEndpoint = endpoint\n    .in(\"auth\")\n    .tag(\"Auth\")\n    .errorOut(errorResponseErrorOut)\n\n  val login: Endpoint[Unit, Login.Request, ErrorResponse, (Login.Response, String), Any] =\n    baseEndpoint.post\n      .in(\"login\")\n      .in(\n        jsonBody[Login.Request].example(\n          Login.Request(\n            email = Email.trusted(\"alexis@wiringbits.net\"),\n            password = Password.trusted(\"notSoWeakPassword\"),\n            captcha = Captcha.trusted(\"captcha\")\n          )\n        )\n      )\n      .out(\n        jsonBody[Login.Response]\n          .description(\"Successful login\")\n          .example(\n            Login.Response(\n              id = UUID.fromString(\"3fa85f64-5717-4562-b3fc-2c963f66afa6\"),\n              name = Name.trusted(\"Alexis\"),\n              email = Email.trusted(\"alexis@wiringbits.net\")\n            )\n          )\n      )\n      .out(setSessionHeader)\n      .errorOut(oneOf(HttpErrors.badRequest))\n      .summary(\"Log into the app\")\n      .description(\"Sets a session cookie to authenticate the following requests\")\n\n  def logout(implicit\n      authHandler: ServerRequest => Future[UUID]\n  ): Endpoint[Unit, Future[UUID], ErrorResponse, (Logout.Response, String), Any] =\n    baseEndpoint.post\n      .in(\"logout\")\n      .in(userAuth)\n      .out(jsonBody[Logout.Response].description(\"Successful logout\").example(Logout.Response()))\n      .out(setSessionHeader)\n      .errorOut(oneOf(HttpErrors.badRequest))\n      .summary(\"Logout from the app\")\n      .description(\"Clears the session cookie that's stored securely\")\n\n  def getCurrentUser(implicit\n      authHandler: ServerRequest => Future[UUID]\n  ): Endpoint[Unit, Future[UUID], ErrorResponse, GetCurrentUser.Response, Any] =\n    baseEndpoint.get\n      .in(\"me\")\n      .in(userAuth)\n      .out(\n        jsonBody[GetCurrentUser.Response]\n          .description(\"Got user details\")\n          .example(\n            GetCurrentUser.Response(\n              id = UUID.fromString(\"3fa85f64-5717-4562-b3fc-2c963f66afa6\"),\n              name = Name.trusted(\"Alexis\"),\n              email = Email.trusted(\"alexis@wiringbits.net\"),\n              createdAt = Instant.parse(\"2021-01-01T00:00:00Z\")\n            )\n          )\n      )\n      .summary(\"Get the details for the authenticated user\")\n\n  def routes(implicit authHandler: ServerRequest => Future[UUID]): List[AnyEndpoint] = List(\n    login,\n    logout,\n    getCurrentUser\n  )\n}\n"
  },
  {
    "path": "lib/api/shared/src/main/scala/net/wiringbits/api/endpoints/EnvironmentConfigEndpoints.scala",
    "content": "package net.wiringbits.api.endpoints\n\nimport net.wiringbits.api.models\nimport net.wiringbits.api.models.ErrorResponse\nimport net.wiringbits.api.models.environmentconfig.GetEnvironmentConfig\nimport sttp.tapir.*\nimport sttp.tapir.json.play.*\n\nobject EnvironmentConfigEndpoints {\n  private val baseEndpoint = endpoint\n    .in(\"environment-config\")\n    .tag(\"Misc\")\n    .errorOut(errorResponseErrorOut)\n\n  val getEnvironmentConfig: Endpoint[Unit, Unit, ErrorResponse, GetEnvironmentConfig.Response, Any] =\n    baseEndpoint.get\n      .out(\n        jsonBody[GetEnvironmentConfig.Response]\n          .description(\"Got the config values\")\n          .example(GetEnvironmentConfig.Response(\"siteKey\"))\n      )\n      .summary(\"Get the config values for the current environment\")\n      .description(\"These values are required by the frontend app to interact with the backend\")\n\n  val routes: List[AnyEndpoint] = List(\n    getEnvironmentConfig\n  )\n}\n"
  },
  {
    "path": "lib/api/shared/src/main/scala/net/wiringbits/api/endpoints/HealthEndpoints.scala",
    "content": "package net.wiringbits.api.endpoints\n\nimport sttp.tapir.*\n\nobject HealthEndpoints {\n  private val baseEndpoint = endpoint\n    .tag(\"Misc\")\n    .in(\"health\")\n\n  val check: Endpoint[Unit, Unit, Unit, Unit, Any] = baseEndpoint.get\n    .out(emptyOutput.description(\"The app is healthy\"))\n    .summary(\"Queries the application's health\")\n\n  val routes: List[AnyEndpoint] = List(\n    check\n  )\n}\n"
  },
  {
    "path": "lib/api/shared/src/main/scala/net/wiringbits/api/endpoints/UsersEndpoints.scala",
    "content": "package net.wiringbits.api.endpoints\n\nimport net.wiringbits.api.models.*\nimport net.wiringbits.api.models.users.*\nimport net.wiringbits.common.models.*\nimport sttp.tapir.*\nimport sttp.tapir.json.play.*\nimport sttp.tapir.model.ServerRequest\n\nimport java.time.Instant\nimport java.util.UUID\nimport scala.concurrent.Future\n\nobject UsersEndpoints {\n  private val baseEndpoint = endpoint\n    .in(\"users\")\n    .tag(\"Users\")\n    .errorOut(errorResponseErrorOut)\n\n  val create: Endpoint[Unit, CreateUser.Request, ErrorResponse, CreateUser.Response, Any] = baseEndpoint.post\n    .in(\n      jsonBody[CreateUser.Request].example(\n        CreateUser.Request(\n          name = Name.trusted(\"Alexis\"),\n          email = Email.trusted(\"alexis@wiringbits.net\"),\n          password = Password.trusted(\"notSoWeakPassword\"),\n          captcha = Captcha.trusted(\"captcha\")\n        )\n      )\n    )\n    .out(\n      jsonBody[CreateUser.Response]\n        .description(\"The account was created\")\n        .example(\n          CreateUser.Response(\n            id = UUID.fromString(\"3fa85f64-5717-4562-b3fc-2c963f66afa6\"),\n            name = Name.trusted(\"Alexis\"),\n            email = Email.trusted(\"alexis@wiringbits.net\")\n          )\n        )\n    )\n    .errorOut(oneOf(HttpErrors.badRequest))\n    .summary(\"Creates a new account\")\n    .description(\"Requires a captcha\")\n\n  val verifyEmail: Endpoint[Unit, VerifyEmail.Request, ErrorResponse, VerifyEmail.Response, Any] = baseEndpoint.post\n    .in(\"verify-email\")\n    .in(\n      jsonBody[VerifyEmail.Request].example(\n        VerifyEmail.Request(\n          UserToken(\n            userId = UUID.fromString(\"3fa85f64-5717-4562-b3fc-2c963f66afa6\"),\n            token = UUID.fromString(\"3fa85f64-5717-4562-b3fc-2c963f66afa6\")\n          )\n        )\n      )\n    )\n    .out(jsonBody[VerifyEmail.Response].description(\"The account's email was verified\").example(VerifyEmail.Response()))\n    .errorOut(oneOf(HttpErrors.badRequest))\n    .summary(\"Verify the user's email\")\n    .description(\n      \"When an account is created, a verification code is sent to the registered email, this operations take such code and marks the email as verified\"\n    )\n\n  val forgotPassword: Endpoint[Unit, ForgotPassword.Request, ErrorResponse, ForgotPassword.Response, Any] =\n    baseEndpoint.post\n      .in(\"forgot-password\")\n      .in(\n        jsonBody[ForgotPassword.Request].example(\n          ForgotPassword.Request(\n            email = Email.trusted(\"alexis@wirngbits.net\"),\n            captcha = Captcha.trusted(\"captcha\")\n          )\n        )\n      )\n      .out(\n        jsonBody[ForgotPassword.Response]\n          .description(\"The email to recover the password was sent\")\n          .example(ForgotPassword.Response())\n      )\n      .errorOut(oneOf(HttpErrors.badRequest))\n      .summary(\"Requests an email to reset a user password\")\n\n  val resetPassword: Endpoint[Unit, ResetPassword.Request, ErrorResponse, ResetPassword.Response, Any] =\n    baseEndpoint.post\n      .in(\"reset-password\")\n      .in(\n        jsonBody[ResetPassword.Request]\n          .example(\n            ResetPassword.Request(\n              token = UserToken(\n                userId = UUID.fromString(\"3fa85f64-5717-4562-b3fc-2c963f66afa6\"),\n                token = UUID.fromString(\"3fa85f64-5717-4562-b3fc-2c963f66afa6\")\n              ),\n              password = Password.trusted(\"notSoWeakPassword\")\n            )\n          )\n      )\n      .out(\n        jsonBody[ResetPassword.Response]\n          .description(\"The password was updated\")\n          .example(\n            ResetPassword.Response(\n              name = Name.trusted(\"Alexis\"),\n              email = Email.trusted(\"alexis@wiringbits.net\")\n            )\n          )\n      )\n      .errorOut(oneOf[Unit](HttpErrors.badRequest))\n      .summary(\"Resets a user password\")\n\n  val sendEmailVerificationToken\n      : Endpoint[Unit, SendEmailVerificationToken.Request, ErrorResponse, SendEmailVerificationToken.Response, Any] =\n    baseEndpoint.post\n      .in(\"email-verification-token\")\n      .in(\n        jsonBody[SendEmailVerificationToken.Request].example(\n          SendEmailVerificationToken.Request(\n            email = Email.trusted(\"alexis@wiringbits.net\"),\n            captcha = Captcha.trusted(\"captcha\")\n          )\n        )\n      )\n      .out(\n        jsonBody[SendEmailVerificationToken.Response]\n          .description(\"The account's email was verified\")\n          .example(\n            SendEmailVerificationToken.Response(\n              expiresAt = Instant.parse(\"2021-01-01T00:00:00Z\")\n            )\n          )\n      )\n      .errorOut(oneOf(HttpErrors.badRequest))\n      .summary(\"Sends the email verification token\")\n      .description(\n        \"The user's email should be unconfirmed, this is intended to re-send a token in case the previous one did not arrive\"\n      )\n\n  def update(implicit\n      authHandler: ServerRequest => Future[UUID]\n  ): Endpoint[Unit, (UpdateUser.Request, Future[UUID]), ErrorResponse, UpdateUser.Response, Any] =\n    baseEndpoint.put\n      .in(\"me\")\n      .in(\n        jsonBody[UpdateUser.Request].example(\n          UpdateUser.Request(\n            name = Name.trusted(\"Alexis\")\n          )\n        )\n      )\n      .in(userAuth)\n      .out(jsonBody[UpdateUser.Response].description(\"The user details were updated\").example(UpdateUser.Response()))\n      .errorOut(oneOf(HttpErrors.badRequest))\n      .summary(\"Updates the authenticated user details\")\n\n  def updatePassword(implicit\n      authHandler: ServerRequest => Future[UUID]\n  ): Endpoint[Unit, (UpdatePassword.Request, Future[UUID]), ErrorResponse, UpdatePassword.Response, Any] =\n    baseEndpoint.put\n      .in(\"me\" / \"password\")\n      .in(\n        jsonBody[UpdatePassword.Request]\n          .description(\"The user password was updated\")\n          .example(\n            UpdatePassword.Request(\n              oldPassword = Password.trusted(\"oldWeakPassword\"),\n              newPassword = Password.trusted(\"newNotSoWeakPassword\")\n            )\n          )\n      )\n      .in(userAuth)\n      .out(jsonBody[UpdatePassword.Response])\n      .errorOut(oneOf(HttpErrors.badRequest))\n      .summary(\"Updates the authenticated user password\")\n\n  def getLogs(implicit\n      authHandler: ServerRequest => Future[UUID]\n  ): Endpoint[Unit, Future[UUID], ErrorResponse, GetUserLogs.Response, Any] = baseEndpoint.get\n    .in(\"me\" / \"logs\")\n    .in(userAuth)\n    .out(\n      jsonBody[GetUserLogs.Response]\n        .description(\"Got user logs\")\n        .example(\n          GetUserLogs.Response(\n            List(\n              GetUserLogs.Response.UserLog(\n                userLogId = UUID.fromString(\"3fa85f64-5717-4562-b3fc-2c963f66afa6\"),\n                message = \"Message\",\n                createdAt = Instant.parse(\"2021-01-01T00:00:00Z\")\n              )\n            )\n          )\n        )\n    )\n    .errorOut(oneOf(HttpErrors.badRequest))\n    .summary(\"Get the logs for the authenticated user\")\n\n  def routes(implicit authHandler: ServerRequest => Future[UUID]): List[AnyEndpoint] = List(\n    create,\n    verifyEmail,\n    forgotPassword,\n    resetPassword,\n    sendEmailVerificationToken,\n    update,\n    updatePassword,\n    getLogs\n  )\n}\n"
  },
  {
    "path": "lib/api/shared/src/main/scala/net/wiringbits/api/endpoints/package.scala",
    "content": "package net.wiringbits.api\n\nimport net.wiringbits.api.models.{ErrorResponse, errorResponseFormat}\nimport sttp.model.StatusCode\nimport sttp.tapir.*\nimport sttp.tapir.EndpointInput.AuthType\nimport sttp.tapir.generic.auto.*\nimport sttp.tapir.json.play.*\nimport sttp.tapir.model.ServerRequest\n\nimport java.util.UUID\nimport scala.concurrent.Future\n\npackage object endpoints {\n  // TODO: better name?\n  object HttpErrors {\n    val badRequest: EndpointOutput.OneOfVariant[Unit] = oneOfVariant(\n      statusCode(StatusCode.BadRequest).description(\"Invalid or missing arguments\")\n    )\n\n    val unauthorized: EndpointOutput.OneOfVariant[Unit] = oneOfVariant(\n      statusCode(StatusCode.Unauthorized).description(\"Invalid or missing authentication\")\n    )\n  }\n\n  val adminHeader: EndpointIO.Header[String] = header[String](\"X-Forwarded-User\")\n    .default(\"Unknown\")\n    .schema(_.hidden(true))\n\n  val adminAuth: EndpointInput.Auth[String, AuthType.Http] = auth\n    .basic[String]()\n    .securitySchemeName(\"Basic authorization\")\n    .description(\"Admin credentials\")\n\n  val setSessionHeader: EndpointIO.Header[String] = header[String](\"Set-Cookie\")\n    .description(\"Set user session\")\n    .schema(_.hidden(true))\n\n  val errorResponseErrorOut: EndpointIO.Body[String, ErrorResponse] = jsonBody[ErrorResponse]\n    .description(\"Error response\")\n    .example(ErrorResponse(\"Unauthorized: Invalid or missing authentication\"))\n    .schema(_.hidden(true))\n\n  def userAuth(implicit handleAuth: ServerRequest => Future[UUID]): EndpointInput.ExtractFromRequest[Future[UUID]] =\n    extractFromRequest(handleAuth)\n}\n"
  },
  {
    "path": "lib/api/shared/src/main/scala/net/wiringbits/api/models/PlayErrorResponse.scala",
    "content": "package net.wiringbits.api.models\n\nimport play.api.libs.json.{Format, Json}\nimport sttp.tapir.Schema\n\n// play json errors are like:\n// {\"error\":{\"requestId\":2,\"message\":\"Invalid Json: ...\"}}\n\ncase class PlayErrorResponse(error: PlayErrorResponse.PlayError)\n\nobject PlayErrorResponse {\n  case class PlayError(message: String)\n\n  implicit val playErrorResponseErrorFormat: Format[PlayError] = Json.format[PlayError]\n  implicit val playErrorResponseFormat: Format[PlayErrorResponse] = Json.format[PlayErrorResponse]\n\n  implicit val playErrorResponseErrorSchema: Schema[PlayError] =\n    Schema.derived[PlayError].name(Schema.SName(\"PlayError\"))\n  implicit val playErrorResponseSchema: Schema[PlayErrorResponse] = Schema\n    .derived[PlayErrorResponse]\n    .name(Schema.SName(\"PlayErrorResponse\"))\n    .description(\"Response with an application error\")\n}\n"
  },
  {
    "path": "lib/api/shared/src/main/scala/net/wiringbits/api/models/admin/AdminGetUserLogs.scala",
    "content": "package net.wiringbits.api.models.admin\n\nimport net.wiringbits.api.models.*\nimport play.api.libs.json.{Format, Json}\nimport sttp.tapir.Schema\nimport sttp.tapir.generic.auto.*\n\nimport java.time.Instant\nimport java.util.UUID\n\nobject AdminGetUserLogs {\n  case class Response(data: List[Response.UserLog])\n  implicit val adminGetUserLogsResponseFormat: Format[Response] = Json.format[Response]\n  implicit val adminGetUserLogsResponseSchema: Schema[Response] = Schema\n    .derived[Response]\n    .name(Schema.SName(\"AdminGetUserLogsResponse\"))\n    .description(\"Includes the logs for a single user\")\n\n  object Response {\n    case class UserLog(userLogId: UUID, message: String, createdAt: Instant)\n    implicit val adminGetUserLogsResponseUserLogFormat: Format[UserLog] = Json.format[UserLog]\n    implicit val adminGetUserLogsResponseUserLogSchema: Schema[UserLog] = Schema\n      .derived[UserLog]\n      .name(Schema.SName(\"AdminGetUserLogsResponseUserLog\"))\n  }\n}\n"
  },
  {
    "path": "lib/api/shared/src/main/scala/net/wiringbits/api/models/admin/AdminGetUsers.scala",
    "content": "package net.wiringbits.api.models.admin\n\nimport net.wiringbits.api.models.*\nimport net.wiringbits.common.models.{Email, Name}\nimport play.api.libs.json.{Format, Json}\nimport sttp.tapir.Schema\nimport sttp.tapir.generic.auto.*\n\nimport java.time.Instant\nimport java.util.UUID\n\nobject AdminGetUsers {\n\n  case class Response(data: List[Response.User])\n  implicit val adminGetUsersResponseFormat: Format[Response] = Json.format[Response]\n  implicit val adminGetUsersResponseSchema: Schema[Response] = Schema\n    .derived[Response]\n    .name(Schema.SName(\"AdminGetUsersResponse\"))\n    .description(\"Includes the user list\")\n\n  object Response {\n    case class User(id: UUID, name: Name, email: Email, createdAt: Instant)\n    implicit val adminGetUsersResponseUserFormat: Format[User] = Json.format[User]\n    implicit val adminGetUsersResponseUserSchema: Schema[User] = Schema\n      .derived[User]\n      .name(Schema.SName(\"AdminGetUsersResponseUser\"))\n  }\n}\n"
  },
  {
    "path": "lib/api/shared/src/main/scala/net/wiringbits/api/models/auth/GetCurrentUser.scala",
    "content": "package net.wiringbits.api.models.auth\n\nimport net.wiringbits.api.models.*\nimport net.wiringbits.common.models.{Email, Name}\nimport play.api.libs.json.{Format, Json}\nimport sttp.tapir.Schema\n\nimport java.time.Instant\nimport java.util.UUID\n\nobject GetCurrentUser {\n  case class Response(id: UUID, name: Name, email: Email, createdAt: Instant)\n\n  implicit val getUserResponseFormat: Format[Response] = Json.format[Response]\n\n  implicit val getUserResponseSchema: Schema[Response] = Schema\n    .derived[Response]\n    .name(Schema.SName(\"GetCurrentUserResponse\"))\n    .description(\"Response to find the authenticated user details\")\n}\n"
  },
  {
    "path": "lib/api/shared/src/main/scala/net/wiringbits/api/models/auth/Login.scala",
    "content": "package net.wiringbits.api.models.auth\n\nimport net.wiringbits.api.models.*\nimport net.wiringbits.common.models.{Captcha, Email, Name, Password}\nimport play.api.libs.json.{Format, Json}\nimport sttp.tapir.Schema\nimport sttp.tapir.generic.auto.*\n\nimport java.util.UUID\n\nobject Login {\n\n  case class Request(email: Email, password: Password, captcha: Captcha)\n\n  case class Response(id: UUID, name: Name, email: Email)\n\n  implicit val loginRequestFormat: Format[Request] = Json.format[Request]\n  implicit val loginResponseFormat: Format[Response] = Json.format[Response]\n\n  implicit val loginRequestSchema: Schema[Request] = Schema\n    .derived[Request]\n    .name(Schema.SName(\"LoginRequest\"))\n    .description(\"Request to log into the app\")\n  implicit val loginResponseSchema: Schema[Response] = Schema\n    .derived[Response]\n    .name(Schema.SName(\"LoginResponse\"))\n    .description(\"Response after logging into the app\")\n}\n"
  },
  {
    "path": "lib/api/shared/src/main/scala/net/wiringbits/api/models/auth/Logout.scala",
    "content": "package net.wiringbits.api.models.auth\n\nimport play.api.libs.json.{Format, Json}\nimport sttp.tapir.Schema\n\nobject Logout {\n\n  case class Request(noData: String = \"\")\n\n  case class Response(noData: String = \"\")\n\n  implicit val logoutRequestFormat: Format[Request] = Json.format[Request]\n  implicit val logoutResponseFormat: Format[Response] = Json.format[Response]\n\n  implicit val logoutRequestSchema: Schema[Request] = Schema\n    .derived[Request]\n    .name(Schema.SName(\"LogoutRequest\"))\n    .description(\"Request to log out of the app\")\n  implicit val logoutResponseSchema: Schema[Response] = Schema\n    .derived[Response]\n    .name(Schema.SName(\"LogoutResponse\"))\n    .description(\"Response after logging out of the app\")\n}\n"
  },
  {
    "path": "lib/api/shared/src/main/scala/net/wiringbits/api/models/environmentconfig/GetEnvironmentConfig.scala",
    "content": "package net.wiringbits.api.models.environmentconfig\n\nimport play.api.libs.json.{Format, Json}\nimport sttp.tapir.Schema\n\nobject GetEnvironmentConfig {\n  case class Response(recaptchaSiteKey: String)\n\n  implicit val configResponseFormat: Format[Response] = Json.format[Response]\n\n  implicit val configResponseSchema: Schema[Response] = Schema\n    .derived[Response]\n    .name(Schema.SName(\"GetEnvironmentConfigResponse\"))\n    .description(\"Request to fetch the environment config\")\n}\n"
  },
  {
    "path": "lib/api/shared/src/main/scala/net/wiringbits/api/models/package.scala",
    "content": "package net.wiringbits.api\n\nimport net.wiringbits.webapp.common.models.WrappedString\nimport play.api.libs.json.*\nimport sttp.tapir.generic.auto.*\nimport sttp.tapir.{Schema, SchemaType}\n\nimport java.time.Instant\n\npackage object models {\n\n  /** For some reason, play-json doesn't provide support for Instant in the scalajs version, grabbing the jvm values\n    * seems to work:\n    *   - https://github.com/playframework/play-json/blob/master/play-json/jvm/src/main/scala/play/api/libs/json/EnvReads.scala\n    *   - https://github.com/playframework/play-json/blob/master/play-json/jvm/src/main/scala/play/api/libs/json/EnvWrites.scala\n    */\n  implicit val instantFormat: Format[Instant] = Format[Instant](\n    fjs = implicitly[Reads[String]].map(string => Instant.parse(string)),\n    tjs = Writes[Instant](i => JsString(i.toString))\n  )\n\n  case class ErrorResponse(error: String)\n  implicit val errorResponseFormat: Format[ErrorResponse] = Json.format[ErrorResponse]\n  implicit val errorResponseSchema: Schema[ErrorResponse] = Schema\n    .derived[ErrorResponse]\n    .name(Schema.SName(\"ErrorResponse\"))\n\n  implicit def wrappedStringSchema[T <: WrappedString]: Schema[T] = Schema(SchemaType.SString())\n}\n"
  },
  {
    "path": "lib/api/shared/src/main/scala/net/wiringbits/api/models/users/CreateUser.scala",
    "content": "package net.wiringbits.api.models.users\n\nimport net.wiringbits.api.models.*\nimport net.wiringbits.common.models.{Captcha, Email, Name, Password}\nimport play.api.libs.json.{Format, Json}\nimport sttp.tapir.Schema\n\nimport java.util.UUID\n\nobject CreateUser {\n  case class Request(name: Name, email: Email, password: Password, captcha: Captcha)\n  case class Response(id: UUID, name: Name, email: Email)\n\n  implicit val createUserRequestFormat: Format[Request] = Json.format[Request]\n  implicit val createUserResponseFormat: Format[Response] = Json.format[Response]\n\n  implicit val createUserRequestSchema: Schema[Request] = Schema\n    .derived[Request]\n    .name(Schema.SName(\"CreateUserRequest\"))\n    .description(\"Request for the create user API\")\n  implicit val createUserResponseSchema: Schema[Response] = Schema\n    .derived[Response]\n    .name(Schema.SName(\"CreateUserResponse\"))\n    .description(\"Response for the create user API\")\n}\n"
  },
  {
    "path": "lib/api/shared/src/main/scala/net/wiringbits/api/models/users/ForgotPassword.scala",
    "content": "package net.wiringbits.api.models.users\n\nimport net.wiringbits.api.models.*\nimport net.wiringbits.common.models.{Captcha, Email}\nimport play.api.libs.json.{Format, Json}\nimport sttp.tapir.Schema\n\nobject ForgotPassword {\n  case class Request(email: Email, captcha: Captcha)\n\n  case class Response(noData: String = \"\")\n\n  implicit val forgotPasswordRequestFormat: Format[Request] = Json.format[Request]\n  implicit val forgotPasswordResponseFormat: Format[Response] = Json.format[Response]\n\n  implicit val forgotPasswordRequestSchema: Schema[Request] = Schema\n    .derived[Request]\n    .name(Schema.SName(\"ForgotPasswordRequest\"))\n    .description(\"Request to reset a forgotten password\")\n  implicit val forgotPasswordResponseSchema: Schema[Response] = Schema\n    .derived[Response]\n    .name(Schema.SName(\"ForgotPasswordResponse\"))\n    .description(\"Response to the ForgotPasswordRequest\")\n}\n"
  },
  {
    "path": "lib/api/shared/src/main/scala/net/wiringbits/api/models/users/GetUserLogs.scala",
    "content": "package net.wiringbits.api.models.users\n\nimport net.wiringbits.api.models.*\nimport play.api.libs.json.{Format, Json}\nimport sttp.tapir.Schema\nimport sttp.tapir.generic.auto.*\n\nimport java.time.Instant\nimport java.util.UUID\n\nobject GetUserLogs {\n  case class Response(data: List[Response.UserLog])\n\n  object Response {\n    case class UserLog(userLogId: UUID, message: String, createdAt: Instant)\n    implicit val getUserLogsResponseFormat: Format[UserLog] = Json.format[UserLog]\n    implicit val getUserLogsResponseSchema: Schema[UserLog] =\n      Schema.derived[UserLog].name(Schema.SName(\"GetUserLogsResponseUserLog\"))\n  }\n\n  implicit val getUserLogsResponseFormat: Format[Response] = Json.format[Response]\n\n  implicit val getUserLogsResponseSchema: Schema[Response] = Schema\n    .derived[Response]\n    .name(Schema.SName(\"GetUserLogsResponse\"))\n    .description(\"Includes the authenticated user logs\")\n}\n"
  },
  {
    "path": "lib/api/shared/src/main/scala/net/wiringbits/api/models/users/ResetPassword.scala",
    "content": "package net.wiringbits.api.models.users\n\nimport net.wiringbits.api.models.*\nimport net.wiringbits.common.models.{Email, Name, Password, UserToken}\nimport play.api.libs.json.{Format, Json}\nimport sttp.tapir.Schema\nimport sttp.tapir.generic.auto.*\n\nobject ResetPassword {\n\n  case class Request(token: UserToken, password: Password)\n\n  case class Response(name: Name, email: Email)\n\n  implicit val resetPasswordRequestFormat: Format[Request] = Json.format[Request]\n  implicit val resetPasswordResponseFormat: Format[Response] = Json.format[Response]\n\n  implicit val resetPasswordRequestSchema: Schema[Request] = Schema\n    .derived[Request]\n    .name(Schema.SName(\"ResetPasswordRequest\"))\n    .description(\"Request to reset a user password\")\n  implicit val resetPasswordResponseSchema: Schema[Response] = Schema\n    .derived[Response]\n    .name(Schema.SName(\"ResetPasswordResponse\"))\n    .description(\"Response after resetting a user password\")\n}\n"
  },
  {
    "path": "lib/api/shared/src/main/scala/net/wiringbits/api/models/users/SendEmailVerificationToken.scala",
    "content": "package net.wiringbits.api.models.users\n\nimport net.wiringbits.api.models.*\nimport net.wiringbits.common.models.{Captcha, Email}\nimport play.api.libs.json.{Format, Json}\nimport sttp.tapir.Schema\n\nimport java.time.Instant\n\nobject SendEmailVerificationToken {\n\n  case class Request(email: Email, captcha: Captcha)\n\n  case class Response(expiresAt: Instant)\n\n  implicit val sendEmailVerificationTokenRequestFormat: Format[Request] = Json.format[Request]\n  implicit val sendEmailVerificationTokenResponseFormat: Format[Response] = Json.format[Response]\n\n  implicit val sendEmailVerificationTokenRequestSchema: Schema[Request] = Schema\n    .derived[Request]\n    .name(Schema.SName(\"SendEmailVerificationTokenRequest\"))\n    .description(\"Request to re-send the token to verify an email\")\n  implicit val sendEmailVerificationTokenResponseSchema: Schema[Response] = Schema\n    .derived[Response]\n    .name(Schema.SName(\"SendEmailVerificationTokenResponse\"))\n    .description(\"Response after sending the token to verify an email\")\n}\n"
  },
  {
    "path": "lib/api/shared/src/main/scala/net/wiringbits/api/models/users/UpdatePassword.scala",
    "content": "package net.wiringbits.api.models.users\n\nimport net.wiringbits.api.models.*\nimport net.wiringbits.common.models.Password\nimport play.api.libs.json.{Format, Json}\nimport sttp.tapir.Schema\n\nobject UpdatePassword {\n\n  case class Request(oldPassword: Password, newPassword: Password)\n\n  case class Response(noData: String = \"\")\n\n  implicit val updatePasswordRequestFormat: Format[Request] = Json.format[Request]\n  implicit val updatePasswordResponseFormat: Format[Response] = Json.format[Response]\n\n  implicit val updatePasswordRequestSchema: Schema[Request] = Schema\n    .derived[Request]\n    .name(Schema.SName(\"UpdatePasswordRequest\"))\n    .description(\"Request to change the user's password\")\n  implicit val updatePasswordResponseSchema: Schema[Response] = Schema\n    .derived[Response]\n    .name(Schema.SName(\"UpdatePasswordResponse\"))\n    .description(\"Response after updating the user's password\")\n}\n"
  },
  {
    "path": "lib/api/shared/src/main/scala/net/wiringbits/api/models/users/UpdateUser.scala",
    "content": "package net.wiringbits.api.models.users\n\nimport net.wiringbits.api.models.*\nimport net.wiringbits.common.models.Name\nimport play.api.libs.json.{Format, Json}\nimport sttp.tapir.Schema\n\nobject UpdateUser {\n\n  case class Request(name: Name)\n\n  case class Response(noData: String = \"\")\n\n  implicit val updateUserRequestFormat: Format[Request] = Json.format[Request]\n  implicit val updateUserResponseFormat: Format[Response] = Json.format[Response]\n\n  implicit val updateUserRequestSchema: Schema[Request] = Schema\n    .derived[Request]\n    .name(Schema.SName(\"UpdateUserRequest\"))\n    .description(\"Request to update user details\")\n  implicit val updateUserResponseSchema: Schema[Response] = Schema\n    .derived[Response]\n    .name(Schema.SName(\"UpdateUserResponse\"))\n    .description(\"Response after updating the user details\")\n}\n"
  },
  {
    "path": "lib/api/shared/src/main/scala/net/wiringbits/api/models/users/VerifyEmail.scala",
    "content": "package net.wiringbits.api.models.users\n\nimport net.wiringbits.common.models.UserToken\nimport play.api.libs.json.{Format, Json}\nimport sttp.tapir.Schema\nimport sttp.tapir.generic.auto.*\n\nobject VerifyEmail {\n\n  case class Request(token: UserToken)\n\n  case class Response(noData: String = \"\")\n\n  implicit val verifyEmailRequestFormat: Format[Request] = Json.format[Request]\n  implicit val verifyEmailResponseFormat: Format[Response] = Json.format[Response]\n\n  implicit val verifyEmailRequestSchema: Schema[Request] = Schema\n    .derived[Request]\n    .name(Schema.SName(\"VerifyEmailRequest\"))\n    .description(\"Request to verify an email\")\n  implicit val verifyEmailResponseSchema: Schema[Response] = Schema\n    .derived[Response]\n    .name(Schema.SName(\"VerifyEmailResponse\"))\n    .description(\"Response after verifying an email\")\n}\n"
  },
  {
    "path": "lib/api/shared/src/main/scala/net/wiringbits/api/utils/Formatter.scala",
    "content": "package net.wiringbits.api.utils\n\nimport java.time.Instant\n\nobject Formatter {\n\n  def instant(item: Instant): String = {\n    try {\n      java.time.ZonedDateTime\n        .ofInstant(item, java.time.ZoneId.systemDefault())\n        .format(java.time.format.DateTimeFormatter.ofPattern(\"dd/MMM/uuuu hh:mm a\"))\n    } catch {\n      // if for any reason the locale is not available in the sjs libraries, the operation will fail\n      // this shouldn't happen in the jvm\n      case _: Throwable => item.toString\n    }\n  }\n}\n"
  },
  {
    "path": "lib/common/js/src/test/scala/java/security/SecureRandom.scala",
    "content": "package java.security\n\nimport scala.scalajs.js\nimport scala.scalajs.js.typedarray.*\n\n// DISCLAIMER: This is almost identical to the official library https://github.com/scala-js/scala-js-fake-insecure-java-securerandom\n// There was the need to apply a patch that won't be accepted by the upstream library, given that this is used\n// only for tests, it shouldn't be a problem to keep the patch.\n//\n// The seed in java.util.Random will be unused, so set to 0L instead of having to generate one\nclass SecureRandom() extends java.util.Random(0L) {\n  // Make sure to resolve the appropriate function no later than the first instantiation\n  private val getRandomValuesFun = SecureRandom.getRandomValuesFun\n\n  /* setSeed has no effect. For cryptographically secure PRNGs, giving a seed\n   * can only ever increase the entropy. It is never allowed to decrease it.\n   * Given that we don't have access to an API to strengthen the entropy of the\n   * underlying PRNG, it's fine to ignore it instead.\n   *\n   * Note that the doc of `SecureRandom` says that it will seed itself upon\n   * first call to `nextBytes` or `next`, if it has not been seeded yet. This\n   * suggests that an *initial* call to `setSeed` would make a `SecureRandom`\n   * instance deterministic. Experimentally, this does not seem to be the case,\n   * however, so we don't spend extra effort to make that happen.\n   */\n  override def setSeed(x: Long): Unit = ()\n\n  override def nextBytes(bytes: Array[Byte]): Unit = {\n    val len = bytes.length\n    val buffer = new Int8Array(len)\n    getRandomValuesFun(buffer)\n    var i = 0\n    while (i != len) {\n      bytes(i) = buffer(i)\n      i += 1\n    }\n  }\n\n  override protected final def next(numBits: Int): Int = {\n    if (numBits <= 0) {\n      0 // special case because the formula on the last line is incorrect for numBits == 0\n    } else {\n      val buffer = new Int32Array(1)\n      getRandomValuesFun(buffer)\n      val rand32 = buffer(0)\n      rand32 & (-1 >>> (32 - numBits)) // Clear the (32 - numBits) higher order bits\n    }\n  }\n}\n\nobject SecureRandom {\n  private lazy val getRandomValuesFun: js.Function1[ArrayBufferView, Unit] = {\n    if (\n      js.typeOf(js.Dynamic.global.crypto) != \"undefined\" &&\n      js.typeOf(js.Dynamic.global.crypto.getRandomValues) == \"function\"\n    ) {\n      { (buffer: ArrayBufferView) =>\n        js.Dynamic.global.crypto.getRandomValues(buffer)\n        ()\n      }\n    } else if (js.typeOf(js.Dynamic.global.require) == \"function\") {\n      try {\n        val crypto = js.Dynamic.global.require(\"crypto\")\n        if (js.typeOf(crypto.randomFillSync) == \"function\") {\n          { (buffer: ArrayBufferView) =>\n            /** This part differs from the official implementation because it catches runtime exceptions\n              *\n              * This was necessary because webpack seems to be polluting our runtime libraries with one that breaks in\n              * the tests.\n              */\n            try {\n              crypto.randomFillSync(buffer)\n            } catch {\n              case _: Throwable => insecureDefault(buffer)\n            }\n            ()\n          }\n        } else {\n          insecureDefault\n        }\n      } catch {\n        case _: Throwable =>\n          insecureDefault\n      }\n    } else {\n      insecureDefault\n    }\n  }\n\n  private def insecureDefault: js.Function1[ArrayBufferView, Unit] = {\n    val insecureRandom = new java.util.Random()\n\n    { (buffer: ArrayBufferView) =>\n      val asInt8Array = new Int8Array(buffer.buffer, buffer.byteOffset, buffer.byteLength)\n      val len = asInt8Array.length\n      val arrayBuffer = new Array[Byte](len)\n      insecureRandom.nextBytes(arrayBuffer)\n      var i = 0\n      while (i != len) {\n        asInt8Array(i) = arrayBuffer(i)\n        i += 1\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "lib/common/shared/src/main/scala/net/wiringbits/common/ErrorMessages.scala",
    "content": "package net.wiringbits.common\n\nobject ErrorMessages {\n  val emailNotVerified = \"The email is not verified, check your spam folder if you don't see the email.\"\n}\n"
  },
  {
    "path": "lib/common/shared/src/main/scala/net/wiringbits/common/models/Captcha.scala",
    "content": "package net.wiringbits.common.models\n\nimport net.wiringbits.webapp.common.models.WrappedString\nimport net.wiringbits.webapp.common.validators.ValidationResult\n\nclass Captcha private (val string: String) extends WrappedString\n\nobject Captcha extends WrappedString.Companion[Captcha] {\n\n  override def validate(string: String): ValidationResult[Captcha] = {\n\n    Option(string.trim)\n      .filter(_.nonEmpty)\n      .map(ValidationResult.Valid(_, new Captcha(string)))\n      .getOrElse {\n        ValidationResult.Invalid(string, \"Invalid recaptcha\")\n      }\n  }\n\n  override def trusted(string: String): Captcha = new Captcha(string)\n}\n"
  },
  {
    "path": "lib/common/shared/src/main/scala/net/wiringbits/common/models/Email.scala",
    "content": "package net.wiringbits.common.models\n\nimport net.wiringbits.webapp.common.models.WrappedString\nimport net.wiringbits.webapp.common.validators.ValidationResult\n\nclass Email private (val string: String) extends WrappedString\n\nobject Email extends WrappedString.Companion[Email] {\n\n  private val emailRegex =\n    \"\"\"^[\\w.!#$%&'*+/=?^_`{|}~-]+@([\\w-]+\\.)+[\\w-]{2,7}$\"\"\".r\n\n  override def validate(string: String): ValidationResult[Email] = {\n    val valid = emailRegex.findAllMatchIn(string).length == 1\n    Option\n      .when(valid)(ValidationResult.Valid(string, new Email(string)))\n      .getOrElse {\n        ValidationResult.Invalid(string, \"Invalid email\")\n      }\n  }\n\n  override def trusted(string: String): Email = new Email(string)\n}\n"
  },
  {
    "path": "lib/common/shared/src/main/scala/net/wiringbits/common/models/Name.scala",
    "content": "package net.wiringbits.common.models\n\nimport net.wiringbits.webapp.common.models.WrappedString\nimport net.wiringbits.webapp.common.validators.ValidationResult\n\nclass Name private (val string: String) extends WrappedString\n\nobject Name extends WrappedString.Companion[Name] {\n\n  private val minNameLength: Int = 2 // we do have people named like `Jo`\n\n  override def validate(string: String): ValidationResult[Name] = {\n    val isValid = string.length >= minNameLength\n\n    Option\n      .when(isValid)(ValidationResult.Valid(string, new Name(string)))\n      .getOrElse {\n        ValidationResult.Invalid(string, \"Invalid name\")\n      }\n  }\n\n  override def trusted(string: String): Name = new Name(string)\n}\n"
  },
  {
    "path": "lib/common/shared/src/main/scala/net/wiringbits/common/models/Password.scala",
    "content": "package net.wiringbits.common.models\n\nimport net.wiringbits.webapp.common.models.WrappedString\nimport net.wiringbits.webapp.common.validators.ValidationResult\n\nclass Password private (val string: String) extends WrappedString\n\nobject Password extends WrappedString.Companion[Password] {\n  private val minPasswordLength: Int = 8\n\n  override def validate(string: String): ValidationResult[Password] = {\n    val isValid = string.length >= minPasswordLength\n\n    Option\n      .when(isValid)(ValidationResult.Valid(string, new Password(string)))\n      .getOrElse {\n        ValidationResult.Invalid(string, \"Invalid password\")\n      }\n  }\n\n  override def trusted(string: String): Password = new Password(string)\n}\n"
  },
  {
    "path": "lib/common/shared/src/main/scala/net/wiringbits/common/models/UserToken.scala",
    "content": "package net.wiringbits.common.models\n\nimport play.api.libs.json.{Format, Json}\n\nimport java.util.UUID\nimport scala.util.Try\n\ncase class UserToken(userId: UUID, token: UUID)\n\nobject UserToken {\n  def validate(tokenStr: String): Option[UserToken] = {\n    val splittedToken = tokenStr.split(\"_\")\n    val isValid = splittedToken.length == 2\n\n    // TODO: Improve this impl\n    Try(\n      Option.when(isValid)(UserToken(UUID.fromString(splittedToken(0)), UUID.fromString(splittedToken(1))))\n    ).toOption.flatten\n  }\n\n  implicit val userTokenFormat: Format[UserToken] = Json.format[UserToken]\n}\n"
  },
  {
    "path": "lib/common/shared/src/test/scala/net/wiringbits/common/models/EmailSpec.scala",
    "content": "package net.wiringbits.common.models\n\nimport org.scalatest.matchers.must.Matchers.*\nimport org.scalatest.wordspec.AnyWordSpec\n\nclass EmailSpec extends AnyWordSpec {\n\n  val valid = List(\n    \"alexis@wiringbits.net\",\n    \"a@xe.com\",\n    \"ejemplo@goo.gl\",\n    \"ejemplo+aqui@e.io\",\n    \"one_mail@test.com\",\n    \"valid.mail@test.xs\",\n    \"valid_@gf.com\",\n    \"test@gmail.co.au\",\n    \"test@gmail.space\"\n  )\n\n  val invalid = List(\n    \"alexis@wiringbits.net.\",\n    \"alexis@wiringbits.net a@xe.net\",\n    \"esto,noes@unemail\",\n    \"esto tampoco@es\",\n    \"@xe.com\",\n    \"hello@\",\n    \"ejemplo@goo\",\n    \".\",\n    \"\"\n  )\n\n  \"validate\" should {\n    valid.foreach { input =>\n      s\"accept valid values: $input\" in {\n        Email.validate(input).isValid must be(true)\n      }\n    }\n\n    invalid.foreach { input =>\n      s\"reject invalid values: $input\" in {\n        Email.validate(input).isValid must be(false)\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "lib/common/shared/src/test/scala/net/wiringbits/common/models/NameSpec.scala",
    "content": "package net.wiringbits.common.models\n\nimport org.scalatest.matchers.must.Matchers.*\nimport org.scalatest.wordspec.AnyWordSpec\n\nclass NameSpec extends AnyWordSpec {\n\n  val valid = List(\n    \"ale\",\n    \"jo\",\n    \"jorge julian\"\n  )\n\n  val invalid = List(\n    \".\",\n    \"\",\n    \"a\"\n  )\n\n  \"validate\" should {\n    valid.foreach { input =>\n      s\"accept valid values: $input\" in {\n        Name.validate(input).isValid must be(true)\n      }\n    }\n\n    invalid.foreach { input =>\n      s\"reject invalid values: $input\" in {\n        Name.validate(input).isValid must be(false)\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "lib/common/shared/src/test/scala/net/wiringbits/common/models/PasswordSpec.scala",
    "content": "package net.wiringbits.common.models\n\nimport org.scalatest.matchers.must.Matchers.*\nimport org.scalatest.wordspec.AnyWordSpec\n\nclass PasswordSpec extends AnyWordSpec {\n\n  val valid = List(\n    \"12345678\",\n    \"aaabbbcc\",\n    \"..121l2.1.2o9z9n23 voi109\"\n  )\n\n  val invalid = List(\n    \"...11..\",\n    \"\",\n    \"1j190u\"\n  )\n\n  \"validate\" should {\n    valid.foreach { input =>\n      s\"accept valid values: $input\" in {\n        Password.validate(input).isValid must be(true)\n      }\n    }\n\n    invalid.foreach { input =>\n      s\"reject invalid values: $input\" in {\n        Password.validate(input).isValid must be(false)\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "lib/common/shared/src/test/scala/net/wiringbits/common/models/UserTokenSpec.scala",
    "content": "package net.wiringbits.common.models\n\nimport org.scalatest.matchers.must.Matchers.{be, must}\nimport org.scalatest.wordspec.AnyWordSpec\n\nimport java.util.UUID\n\nclass UserTokenSpec extends AnyWordSpec {\n\n  \"validate\" should {\n    \"succeed when there's two valid UUIDs and one underscore\" in {\n      val valid = s\"${UUID.randomUUID()}_${UUID.randomUUID()}\"\n      UserToken.validate(valid).isDefined must be(true)\n    }\n\n    s\"fail when the string is not a valid UUID\" in {\n      val invalid = \"wiringbits\"\n      UserToken.validate(invalid).isDefined must be(false)\n    }\n\n    s\"fail when the string is not a valid UUID and there's an underscore\" in {\n      val invalid = \"wiringbits_wiringbits\"\n      UserToken.validate(invalid).isDefined must be(false)\n    }\n\n    s\"fail when there's zero underscores in the string\" in {\n      val invalid = UUID.randomUUID.toString\n      UserToken.validate(invalid).isDefined must be(false)\n    }\n\n    s\"fail when there's more than two underscores in the string\" in {\n      val invalid = s\"${UUID.randomUUID()}_${UUID.randomUUID()}_${UUID.randomUUID()}\"\n      UserToken.validate(invalid).isDefined must be(false)\n    }\n\n    s\"fail when there's an underscore after the UUID\" in {\n      val invalid = s\"${UUID.randomUUID()}_\"\n      UserToken.validate(invalid).isDefined must be(false)\n    }\n  }\n}\n"
  },
  {
    "path": "lib/ui/src/main/scala/net/wiringbits/ui/components/core/widgets/ValidatedTextInput.scala",
    "content": "package net.wiringbits.ui.components.core.widgets\n\nimport com.olvind.mui.muiMaterial.components as mui\n\nimport net.wiringbits.webapp.common.validators.{TextValidator, ValidationResult}\nimport net.wiringbits.webapp.utils.slinkyUtils.forms.FormField\nimport org.scalajs.dom\nimport slinky.core.FunctionalComponent\n\nabstract class ValidatedTextInput[T: TextValidator] {\n  private val validator = implicitly[TextValidator[T]]\n\n  case class Props(\n      field: FormField[T],\n      disabled: Boolean = false,\n      onChange: ValidationResult[T] => Unit,\n      margin: \"dense\" = \"dense\"\n  )\n\n  val component: FunctionalComponent[Props] = FunctionalComponent[Props] { props =>\n    def onChange(text: String): Unit = {\n      val validation = validator(text)\n      props.onChange(validation)\n    }\n\n    val helperText = props.field.value.flatMap(_.errorMessage).getOrElse(\"\")\n    val value = props.field.value.map(_.input).getOrElse(\"\")\n    val hasError = props.field.value.exists(_.hasError)\n    mui.TextField\n      .outlined()\n      .id(s\"ExperimentalTextInput-${props.field.name}\")\n      .name(s\"ExperimentalTextInput-${props.field.name}\")\n      .label(props.field.label)\n      .`type`(props.field.`type`)\n      .required(props.field.required)\n      .fullWidth(true)\n      .disabled(props.disabled)\n      .margin(props.margin)\n      .error(hasError)\n      .helperText(helperText)\n      .value(value)\n      .onChange(e => onChange(e.target.asInstanceOf[dom.HTMLInputElement].value))\n  }\n}\n"
  },
  {
    "path": "lib/ui/src/main/scala/net/wiringbits/ui/components/inputs/inputs.scala",
    "content": "package net.wiringbits.ui.components\n\nimport net.wiringbits.common.models.{Email, Name, Password}\nimport net.wiringbits.ui.components.core.widgets.ValidatedTextInput\n\npackage object inputs {\n  object NameInput extends ValidatedTextInput[Name]\n  object EmailInput extends ValidatedTextInput[Email]\n  object PasswordInput extends ValidatedTextInput[Password]\n}\n"
  },
  {
    "path": "project/build.properties",
    "content": "sbt.version = 1.7.3\n"
  },
  {
    "path": "project/plugins.sbt",
    "content": "// while there are some eviction errors, plugins seem to be compatible so far\nevictionErrorLevel := sbt.util.Level.Warn\n\naddSbtPlugin(\"org.portable-scala\" % \"sbt-scalajs-crossproject\" % \"1.3.1\")\n\naddSbtPlugin(\"org.playframework\" % \"sbt-plugin\" % \"3.0.0\")\n\naddSbtPlugin(\"org.scala-js\" % \"sbt-scalajs\" % \"1.13.1\")\n\naddSbtPlugin(\"ch.epfl.scala\" % \"sbt-scalajs-bundler\" % \"0.20.0\")\n\naddSbtPlugin(\"org.scalablytyped.converter\" % \"sbt-converter\" % \"1.0.0-beta39\")\n\naddSbtPlugin(\"com.eed3si9n\" % \"sbt-buildinfo\" % \"0.11.0\")\n\naddSbtPlugin(\"org.scalameta\" % \"sbt-scalafmt\" % \"2.5.0\")\n\naddSbtPlugin(\"org.wartremover\" % \"sbt-wartremover\" % \"3.1.3\")\n"
  },
  {
    "path": "server/src/main/resources/application.conf",
    "content": "# https://www.playframework.com/documentation/latest/Configuration\n\n# Swagger - be aware these are used at compile time\nswagger {\n  api {\n    basePath = \"\"\n    basePath = ${?SWAGGER_API_BASEPATH}\n\n    info = {\n      version = \"beta\"\n      contact = \"template@wiringbits.net\"\n      title = \"Scala webapp template's API\"\n      description = \"The API for the Scala webapp template app\"\n    }\n  }\n}\n\nplay.i18n.langs = [\"en\"]\n\nplay.filters.hosts {\n  allowed = [host.docker.internal, \"localhost\", \"localhost:9000\", \"127.0.0.1:9000\"]\n  allowed += ${?APP_ALLOWED_HOST_1}\n  allowed += ${?APP_ALLOWED_HOST_2}\n  allowed += ${?APP_ALLOWED_HOST_3}\n\n}\n\nplay.http {\n  # Important for production, it is used to sign sessions\n  secret.key = \"changeme\"\n  secret.key = ${?PLAY_APPLICATION_SECRET}\n\n  errorHandler = \"play.api.http.JsonHttpErrorHandler\"\n\n  session {\n    cookieName = \"__APP_SESSION__\"\n\n    # false by default because we use http locally, must be true in prod\n    secure = false\n    secure = ${?PLAY_SESSION_SECURE}\n\n    # to secure the cookie, this value should be set in prod\n    domain = null\n    domain = ${?PLAY_SESSION_DOMAIN}\n\n    # The session path\n    # Must start with /.\n    path = ${play.http.context}\n    path = ${?PLAY_SESSION_DOMAIN_PATH}\n  }\n}\n\nplay.filters.disabled += \"play.filters.csrf.CSRFFilter\"\nplay.filters.enabled += \"play.filters.cors.CORSFilter\"\n\n\ndb.default {\n  driver = \"org.postgresql.Driver\"\n  host = \"localhost:5432\"\n  database = \"wiringbits_db\"\n  username = \"postgres\"\n  password = \"postgres\"\n\n  host = ${?POSTGRES_HOST}\n  database = ${?POSTGRES_DATABASE}\n  username = ${?POSTGRES_USERNAME}\n  password = ${?POSTGRES_PASSWORD}\n\n  url = \"jdbc:postgresql://\"${db.default.host}\"/\"${db.default.database}\n}\n\nplay.evolutions {\n  autoApply = true\n\n  db.default {\n    enabled = true\n    # Important because when this is false, failed migrations won't get to the play_evolutions table\n    # preventing us to fix them manually\n    autocommit = true\n  }\n}\n\n# Number of database connections\n# See https://github.com/brettwooldridge/HikariCP/wiki/About-Pool-Sizing\nfixedConnectionPool = 9\n\nplay.db {\n  prototype {\n    hikaricp.minimumIdle = ${fixedConnectionPool}\n    hikaricp.maximumPoolSize = ${fixedConnectionPool}\n  }\n}\n\n# Job queue sized to HikariCP connection pool\ndatabase.dispatcher {\n  executor = \"thread-pool-executor\"\n  throughput = 1\n  thread-pool-executor {\n    fixed-pool-size = ${fixedConnectionPool}\n  }\n}\n\nblocking.dispatcher {\n  executor = \"thread-pool-executor\"\n  throughput = 1\n  thread-pool-executor {\n    // very high bound to process lots of blocking operations concurrently\n    fixed-pool-size = 5000\n  }\n}\n\nplay.modules.enabled += \"net.wiringbits.modules.ApisModule\"\nplay.modules.enabled += \"net.wiringbits.modules.ConfigModule\"\nplay.modules.enabled += \"net.wiringbits.modules.ExecutorsModule\"\nplay.modules.enabled += \"net.wiringbits.modules.ClockModule\"\nplay.modules.enabled += \"net.wiringbits.modules.TasksModule\"\n\nemail {\n  senderAddress = \"replace@replace.net\"\n  senderAddress = ${?EMAIL_SENDER_ADDRESS}\n\n  # defines the provider used to send emails, valid values being \"aws\" or \"none\"\n  provider = \"none\"\n  provider = ${?EMAIL_PROVIDER}\n}\n\nuserTokens {\n  hmacSecret = \"REPLACE ME\"\n  hmacSecret = ${?USER_TOKENS_HMAC_SECRET}\n\n  emailVerification {\n    # expiration time for email verification\n    expirationTime = \"24 hours\"\n    expirationTime = ${?USER_TOKENS_EMAIL_VERIFICATION_EXPIRATION_TIME}\n  }\n\n  resetPassword {\n    # expiration time for email reset password\n    expirationTime = \"24 hours\"\n    expirationTime = ${?USER_TOKENS_RESET_PASSWORD_EXPIRATION_TIME}\n  }\n}\n\naws {\n  region = \"us-west-2\"\n  region = ${?AWS_REGION}\n\n  accessKeyId = REPLACE_ME\n  accessKeyId = ${?AWS_ACCESS_KEY_ID}\n\n  secretAccessKey = REPLACE_ME\n  secretAccessKey = ${?AWS_SECRET_ACCESS_KEY}\n}\n\nwebapp {\n  host = \"http://localhost:8080\"\n  host = ${?WEBAPP_HOST}\n}\n\nrecaptcha {\n  # secret key only used for test purposes\n  secretKey = \"6LeIxAcTAAAAAGG-vFI1TnRWxMZNFuojJ4WifJWe\"\n  secretKey = ${?RECAPTCHA_SECRET_KEY}\n  siteKey = \"6LeIxAcTAAAAAJcZVRqyHh71UMIEGNQ_MXjiZKhI\"\n  siteKey = ${?RECAPTCHA_SITE_KEY}\n}\n\nbackgroundJobsExecutorTask {\n  # the task will run every time the period is fullfilled\n  interval = 1 minutes\n  interval = ${?NOTIFICATIONS_TASK_INTERVAL}\n}\n"
  },
  {
    "path": "server/src/main/resources/evolutions/default/1.sql",
    "content": "\n-- !Ups\n\n\n-- The users table has the minimum necessary data\nCREATE TABLE users(\n  user_id UUID NOT NULL,\n  name TEXT NOT NULL,\n  last_name TEXT NULL,\n  email CITEXT NOT NULL,\n  password TEXT NOT NULL,\n  created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),\n  verified_on TIMESTAMPTZ NULL,\n  CONSTRAINT users_user_id_pk PRIMARY KEY (user_id),\n  CONSTRAINT users_email_unique UNIQUE (email)\n);\n\nCREATE INDEX users_email_index ON users USING BTREE (email);\n\n-- create the table to store the user logs\nCREATE TABLE user_logs (\n    user_log_id UUID NOT NULL,\n    user_id UUID NOT NULL,\n    message TEXT NOT NULL,\n    created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),\n    CONSTRAINT user_logs_pk PRIMARY KEY (user_log_id),\n    CONSTRAINT user_logs_users_fk FOREIGN KEY (user_id) REFERENCES users(user_id)\n);\n\nCREATE INDEX user_logs_user_id_index ON user_logs USING BTREE (user_id);\n\nCREATE TABLE user_tokens (\n    user_token_id UUID NOT NULL,\n    token TEXT NOT NULL,\n    token_type TEXT NOT NULL,\n    created_at TIMESTAMPTZ NOT NULL,\n    expires_at TIMESTAMPTZ NOT NULL,\n    user_id UUID NOT NULL,\n    CONSTRAINT user_tokens_id_pk PRIMARY KEY (user_token_id),\n    CONSTRAINT user_tokens_user_id_fk FOREIGN KEY (user_id) REFERENCES users (user_id)\n);\n\nCREATE INDEX user_tokens_user_id_index ON user_tokens USING BTREE (user_id);\n\n-- Stores the notifications we are sending to the user from a background job\nCREATE TABLE user_notifications (\n    user_notification_id UUID NOT NULL,\n    user_id UUID NOT NULL,\n    notification_type TEXT NOT NULL,\n    subject TEXT NOT NULL,\n    message TEXT NOT NULL,\n    status TEXT NOT NULL, -- pending/success/failed,\n    status_details TEXT NULL, -- if failed, what was the reason\n    error_count INT DEFAULT 0,\n    execute_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),\n    created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),\n    updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),\n    CONSTRAINT user_notifications_user_notification_id_pk PRIMARY KEY (user_notification_id),\n    CONSTRAINT user_notifications_user_id_fk FOREIGN KEY (user_id) REFERENCES users(user_id)\n);\n\nCREATE INDEX user_notifications_user_id_index ON user_notifications USING BTREE (user_id);\nCREATE INDEX user_notifications_execute_at_index ON user_notifications USING BTREE (execute_at);"
  },
  {
    "path": "server/src/main/resources/evolutions/default/2.sql",
    "content": "\n-- !Ups\n\n-- Stores the background jobs from the app\nCREATE TABLE background_jobs (\n    background_job_id UUID NOT NULL,\n    type TEXT NOT NULL,\n    payload JSONB NOT NULL,\n    status TEXT NOT NULL, -- pending/success/failed,\n    status_details TEXT NULL, -- if failed, what was the reason\n    error_count INT DEFAULT 0,\n    execute_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),\n    created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),\n    updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),\n    CONSTRAINT background_jobs_id_pk PRIMARY KEY (background_job_id)\n);\n\nCREATE INDEX background_jobs_execute_at_index ON background_jobs USING BTREE (execute_at);\n\n-- these are now handled by background_jobs\nDROP TABLE user_notifications;\n"
  },
  {
    "path": "server/src/main/resources/logback.xml",
    "content": "<!-- https://www.playframework.com/documentation/latest/SettingsLogger -->\n<configuration>\n\n    <conversionRule conversionWord=\"coloredLevel\" converterClass=\"play.api.libs.logback.ColoredLevel\" />\n\n    <appender name=\"FILE\" class=\"ch.qos.logback.core.rolling.RollingFileAppender\">\n        <!-- the user.home is used to persist the logs because the application directory is re-created on every deployment -->\n        <!-- TODO: Find a way to override this so that the this only affects deployments -->\n        <file>${user.home:-.}/logs/application.log</file>\n\n        <rollingPolicy class=\"ch.qos.logback.core.rolling.TimeBasedRollingPolicy\">\n            <!-- daily rollover -->\n            <fileNamePattern>${user.home:-.}/logs/application.%d{yyyy-MM-dd}.log</fileNamePattern>\n\n            <!-- keep 30 days' worth of history capped at 3GB total size -->\n            <maxHistory>30</maxHistory>\n            <totalSizeCap>3GB</totalSizeCap>\n        </rollingPolicy>\n\n        <encoder>\n            <pattern>%date [%level] from %logger in %thread - %message%n%rEx%xException </pattern>\n        </encoder>\n    </appender>\n\n    <appender name=\"STDOUT\" class=\"ch.qos.logback.core.ConsoleAppender\">\n        <encoder>\n            <pattern>%coloredLevel %logger{15} - %message%n%rEx%xException{10} </pattern>\n        </encoder>\n    </appender>\n\n    <appender name=\"ASYNCFILE\" class=\"ch.qos.logback.classic.AsyncAppender\">\n        <appender-ref ref=\"FILE\" />\n    </appender>\n\n    <appender name=\"ASYNCSTDOUT\" class=\"ch.qos.logback.classic.AsyncAppender\">\n        <appender-ref ref=\"STDOUT\" />\n    </appender>\n\n    <logger name=\"play\" level=\"INFO\" />\n    <logger name=\"net.wiringbits\" level=\"DEBUG\" />\n    <logger name=\"controllers\" level=\"DEBUG\" />\n\n    <!-- produces invalid CORS warnings, client is not authorized to call this from its own domain -->\n    <logger name=\"play.filters.cors.CORSFilter\" level=\"ERROR\" />\n    <!-- produces host not allowed warnings, client is not allowed to call the host directly -->\n    <logger name=\"play.filters.hosts.AllowedHostsFilter\" level=\"ERROR\" />\n    <!-- produces CSRF warnings when clients send wrong requests -->\n    <logger name=\"play.filters.csrf.CSRFAction\" level=\"ERROR\" />\n\n    <!-- Off these ones as they are annoying, and anyway we manage configuration ourselves -->\n    <logger name=\"com.avaje.ebean.config.PropertyMapLoader\" level=\"OFF\" />\n    <logger name=\"com.avaje.ebeaninternal.server.core.XmlConfigLoader\" level=\"OFF\" />\n    <logger name=\"com.avaje.ebeaninternal.server.lib.BackgroundThread\" level=\"OFF\" />\n    <logger name=\"com.gargoylesoftware.htmlunit.javascript\" level=\"OFF\" />\n\n    <root level=\"ERROR\">\n        <appender-ref ref=\"ASYNCFILE\" />\n        <appender-ref ref=\"ASYNCSTDOUT\" />\n    </root>\n\n</configuration>\n"
  },
  {
    "path": "server/src/main/resources/messages",
    "content": "# https://www.playframework.com/documentation/latest/ScalaI18N\n"
  },
  {
    "path": "server/src/main/resources/routes",
    "content": "# Routes\n# This file defines all application routes (Higher priority routes first)\n# https://www.playframework.com/documentation/latest/ScalaRouting\n# ~~~~\n\n-> / controllers.ApiRouter\n\n# routes for admin tables (GET, POST, PUT and DELETE)\n#-> / net.wiringbits.webapp.utils.admin.AppRouter\n"
  },
  {
    "path": "server/src/main/scala/PekkoStream.scala",
    "content": "/*\n * Copyright (C) from 2022 The Play Framework Contributors <https://github.com/playframework>, 2011-2021 Lightbend Inc. <https://www.lightbend.com>\n */\n\npackage anorm\n\nimport java.sql.Connection\nimport scala.util.control.NonFatal\nimport scala.concurrent.{Future, Promise}\nimport org.apache.pekko.stream.scaladsl.Source\n\nimport scala.annotation.nowarn\n\n/** Anorm companion for the Pekko Streams.\n  *\n  * @define materialization\n  *   It materializes a [[scala.concurrent.Future]] of [[scala.Int]] containing the number of rows read from the source\n  *   upon completion, and a possible exception if row parsing failed.\n  * @define sqlParam\n  *   the SQL query\n  * @define connectionParam\n  *   the JDBC connection, which must not be closed until the source is materialized.\n  * @define columnAliaserParam\n  *   the column aliaser\n  */\n// From https://github.com/playframework/anorm/blob/main/pekko/src/main/scala/anorm/PekkoStream.scala\n// We are copying this because the anorm.pekko isn't published yet\n// TODO: remove after anorm.pekko is published\nobject PekkoStream {\n\n  /** Returns the rows parsed from the `sql` query as a reactive source.\n    *\n    * $materialization\n    *\n    * @tparam T\n    *   the type of the result elements\n    * @param sql\n    *   $sqlParam\n    * @param parser\n    *   the result (row) parser\n    * @param as\n    *   $columnAliaserParam\n    * @param connection\n    *   $connectionParam\n    *\n    * {{{\n    * import java.sql.Connection\n    *\n    * import scala.concurrent.Future\n    *\n    * import org.apache.pekko.stream.scaladsl.Source\n    *\n    * import anorm._\n    *\n    * def resultSource(implicit con: Connection): Source[String, Future[Int]] = PekkoStream.source(SQL\"SELECT * FROM Test\", SqlParser.scalar[String], ColumnAliaser.empty)\n    * }}}\n    */\n  @SuppressWarnings(Array(\"UnusedMethodParameter\"))\n  def source[T](sql: => Sql, parser: RowParser[T], as: ColumnAliaser)(implicit\n      con: Connection\n  ): Source[T, Future[Int]] = Source.fromGraph(new ResultSource[T](con, sql, as, parser))\n\n  /** Returns the rows parsed from the `sql` query as a reactive source.\n    *\n    * $materialization\n    *\n    * @tparam T\n    *   the type of the result elements\n    * @param sql\n    *   $sqlParam\n    * @param parser\n    *   the result (row) parser\n    * @param connection\n    *   $connectionParam\n    */\n  @SuppressWarnings(Array(\"UnusedMethodParameter\"))\n  def source[T](sql: => Sql, parser: RowParser[T])(implicit con: Connection): Source[T, Future[Int]] =\n    source[T](sql, parser, ColumnAliaser.empty)\n\n  /** Returns the result rows from the `sql` query as an enumerator. This is equivalent to `source[Row](sql,\n    * RowParser.successful, as)`.\n    *\n    * $materialization\n    *\n    * @param sql\n    *   $sqlParam\n    * @param as\n    *   $columnAliaserParam\n    * @param connection\n    *   $connectionParam\n    */\n  def source(sql: => Sql, as: ColumnAliaser)(implicit connection: Connection): Source[Row, Future[Int]] =\n    source(sql, RowParser.successful, as)\n\n  /** Returns the result rows from the `sql` query as an enumerator. This is equivalent to `source[Row](sql,\n    * RowParser.successful, ColumnAliaser.empty)`.\n    *\n    * $materialization\n    *\n    * @param sql\n    *   $sqlParam\n    * @param connection\n    *   $connectionParam\n    */\n  def source(sql: => Sql)(implicit connnection: Connection): Source[Row, Future[Int]] =\n    source(sql, RowParser.successful, ColumnAliaser.empty)\n\n  // Internal stages\n\n  import org.apache.pekko.stream.stage.{GraphStageLogic, GraphStageWithMaterializedValue, OutHandler}\n  import org.apache.pekko.stream.{Attributes, Outlet, SourceShape}\n\n  import java.sql.ResultSet\n  import scala.util.{Failure, Success}\n\n  private[anorm] class ResultSource[T](connection: Connection, sql: Sql, as: ColumnAliaser, parser: RowParser[T])\n      extends GraphStageWithMaterializedValue[SourceShape[T], Future[Int]] {\n\n    @SuppressWarnings(Array(\"org.wartremover.warts.Null\"))\n    private[anorm] var resultSet: ResultSet = _\n\n    override val toString = \"AnormQueryResult\"\n    val out: Outlet[T] = Outlet(s\"${toString}.out\")\n    val shape: SourceShape[T] = SourceShape(out)\n\n    override def createLogicAndMaterializedValue(inheritedAttributes: Attributes): (GraphStageLogic, Future[Int]) = {\n      val result = Promise[Int]()\n\n      val logic = new GraphStageLogic(shape) with OutHandler {\n        private var cursor: Option[Cursor] = None\n        private var counter: Int = 0\n\n        private def failWith(cause: Throwable): Unit = {\n          result.failure(cause)\n          fail(out, cause)\n          ()\n        }\n\n        override def preStart(): Unit = {\n          try {\n            resultSet = sql.unsafeResultSet(connection)\n            nextCursor()\n          } catch {\n            case NonFatal(cause) => failWith(cause)\n          }\n        }\n\n        override def postStop() = release()\n\n        private def release(): Unit = {\n          val stmt: Option[java.sql.Statement] = {\n            if (resultSet != null && !resultSet.isClosed) {\n              val s = resultSet.getStatement\n              resultSet.close()\n              Option(s)\n            } else None\n          }\n\n          stmt.foreach { s =>\n            if (!s.isClosed) s.close()\n          }\n        }\n\n        private def nextCursor(): Unit = {\n          cursor = Sql.unsafeCursor(resultSet, sql.resultSetOnFirstRow, as)\n        }\n\n        def onPull(): Unit = cursor match {\n          case Some(c) =>\n            c.row.as(parser) match {\n              case Success(parsed) => {\n                counter += 1\n                push(out, parsed)\n                nextCursor()\n              }\n\n              case Failure(cause) =>\n                failWith(cause)\n            }\n\n          case _ => {\n            result.success(counter)\n            complete(out)\n          }\n        }\n\n        @nowarn\n        override def onDownstreamFinish() = {\n          result.tryFailure(new InterruptedException(\"Downstream finished\"))\n          release()\n          super.onDownstreamFinish()\n        }\n\n        setHandler(out, this)\n      }\n\n      logic -> result.future\n    }\n  }\n\n}\n"
  },
  {
    "path": "server/src/main/scala/controllers/AdminController.scala",
    "content": "package controllers\n\nimport net.wiringbits.api.endpoints.AdminEndpoints\nimport net.wiringbits.api.models.ErrorResponse\nimport net.wiringbits.api.models.admin.{AdminGetUserLogs, AdminGetUsers}\nimport net.wiringbits.common.models.Email\nimport net.wiringbits.services.AdminService\nimport org.slf4j.LoggerFactory\nimport sttp.capabilities.WebSockets\nimport sttp.capabilities.pekko.PekkoStreams\nimport sttp.tapir.server.ServerEndpoint\n\nimport java.util.UUID\nimport javax.inject.Inject\nimport scala.concurrent.{ExecutionContext, Future}\n\nclass AdminController @Inject() (\n    adminService: AdminService\n)(implicit ec: ExecutionContext) {\n  private val logger = LoggerFactory.getLogger(this.getClass)\n\n  private def getUserLogs(\n      authBasic: String,\n      userId: UUID,\n      adminCookie: String\n  ): Future[Either[ErrorResponse, AdminGetUserLogs.Response]] = handleRequest {\n    logger.info(s\"Get user logs: $userId\")\n    for {\n      response <- adminService.userLogs(userId)\n    } yield Right(response)\n  }\n\n  private def getUsers(\n      authBasic: String,\n      adminCookie: String\n  ): Future[Either[ErrorResponse, AdminGetUsers.Response]] = handleRequest {\n    logger.info(s\"Get users\")\n    for {\n      response <- adminService.users()\n      // TODO: Avoid masking data when this the admin website is not public\n      maskedResponse = response.copy(data = response.data.map(_.copy(email = Email.trusted(\"email@wiringbits.net\"))))\n    } yield Right(maskedResponse)\n  }\n\n  def routes: List[ServerEndpoint[PekkoStreams with WebSockets, Future]] = {\n    List(\n      AdminEndpoints.getUserLogsEndpoint.serverLogic(getUserLogs),\n      AdminEndpoints.getUsersEndpoint.serverLogic(getUsers)\n    )\n  }\n}\n"
  },
  {
    "path": "server/src/main/scala/controllers/ApiRouter.scala",
    "content": "package controllers\n\nimport net.wiringbits.api.endpoints.*\nimport net.wiringbits.config.SwaggerConfig\nimport org.apache.pekko.actor.ActorSystem\nimport org.apache.pekko.stream.Materializer\nimport play.api.routing.Router.Routes\nimport play.api.routing.SimpleRouter\nimport sttp.apispec.openapi.Info\nimport sttp.tapir.AnyEndpoint\nimport sttp.tapir.server.play.PlayServerInterpreter\nimport sttp.tapir.swagger.SwaggerUIOptions\nimport sttp.tapir.swagger.bundle.SwaggerInterpreter\n\nimport javax.inject.Inject\nimport scala.concurrent.{ExecutionContext, Future}\n\nclass ApiRouter @Inject() (\n    adminController: AdminController,\n    authController: AuthController,\n    healthController: HealthController,\n    usersController: UsersController,\n    environmentConfigController: EnvironmentConfigController,\n    swaggerConfig: SwaggerConfig\n)(using ExecutionContext)\n    extends SimpleRouter {\n  given ActorSystem = ActorSystem(\"ApiRouter\")\n\n  private val swagger = SwaggerInterpreter(\n    swaggerUIOptions = SwaggerUIOptions.default.copy(contextPath = List(swaggerConfig.basePath))\n  )\n    .fromEndpoints[Future](\n      ApiRouter.routes,\n      Info(\n        title = swaggerConfig.info.title,\n        version = swaggerConfig.info.version,\n        description = Some(swaggerConfig.info.description)\n      )\n    )\n\n  override def routes: Routes = PlayServerInterpreter()\n    .toRoutes(\n      List(\n        swagger,\n        usersController.routes,\n        authController.routes,\n        healthController.routes,\n        adminController.routes,\n        environmentConfigController.routes\n      ).flatten\n    )\n}\n\nobject ApiRouter {\n  private def routes(implicit ec: ExecutionContext): List[AnyEndpoint] = List(\n    HealthEndpoints.routes,\n    AdminEndpoints.routes,\n    AuthEndpoints.routes,\n    UsersEndpoints.routes,\n    EnvironmentConfigEndpoints.routes\n  ).flatten\n}\n"
  },
  {
    "path": "server/src/main/scala/controllers/AuthController.scala",
    "content": "package controllers\n\nimport net.wiringbits.actions.auth.{GetUserAction, LoginAction}\nimport net.wiringbits.api.endpoints.AuthEndpoints\nimport net.wiringbits.api.models.*\nimport net.wiringbits.api.models.auth.{GetCurrentUser, Login, Logout}\nimport org.slf4j.LoggerFactory\nimport sttp.capabilities.WebSockets\nimport sttp.capabilities.pekko.PekkoStreams\nimport sttp.tapir.server.ServerEndpoint\n\nimport java.util.UUID\nimport javax.inject.Inject\nimport scala.concurrent.{ExecutionContext, Future}\n\nclass AuthController @Inject() (\n    loginAction: LoginAction,\n    getUserAction: GetUserAction,\n    playTapirBridge: PlayTapirBridge\n)(implicit ec: ExecutionContext) {\n  private val logger = LoggerFactory.getLogger(this.getClass)\n\n  private def login(body: Login.Request): Future[Either[ErrorResponse, (Login.Response, String)]] =\n    handleRequest {\n      logger.info(s\"Login API: ${body.email}\")\n      for {\n        response <- loginAction(body)\n        cookieEncoded <- playTapirBridge.setSession(response.id)\n      } yield Right(response, cookieEncoded)\n    }\n\n  private def me(userIdF: Future[UUID]): Future[Either[ErrorResponse, GetCurrentUser.Response]] =\n    handleRequest {\n      for {\n        userId <- userIdF\n        _ = logger.info(s\"Get user info: $userId\")\n        response <- getUserAction(userId)\n      } yield Right(response)\n    }\n\n  private def logout(userIdF: Future[UUID]): Future[Either[ErrorResponse, (Logout.Response, String)]] =\n    handleRequest {\n      for {\n        _ <- userIdF\n        _ = logger.info(\"Logout\")\n        header <- playTapirBridge.clearSession()\n      } yield Right(Logout.Response(), header)\n    }\n\n  def routes: List[ServerEndpoint[PekkoStreams with WebSockets, Future]] = {\n    List(\n      AuthEndpoints.login.serverLogic(login),\n      AuthEndpoints.getCurrentUser.serverLogic(me),\n      AuthEndpoints.logout.serverLogic(logout)\n    )\n  }\n}\n"
  },
  {
    "path": "server/src/main/scala/controllers/EnvironmentConfigController.scala",
    "content": "package controllers\n\nimport net.wiringbits.actions.environmentconfig.GetEnvironmentConfigAction\nimport net.wiringbits.api.endpoints.EnvironmentConfigEndpoints\nimport net.wiringbits.api.models.ErrorResponse\nimport net.wiringbits.api.models.environmentconfig.GetEnvironmentConfig\nimport org.slf4j.LoggerFactory\nimport sttp.capabilities.WebSockets\nimport sttp.capabilities.pekko.PekkoStreams\nimport sttp.tapir.server.ServerEndpoint\n\nimport javax.inject.Inject\nimport scala.concurrent.{ExecutionContext, Future}\n\nclass EnvironmentConfigController @Inject() (\n    getEnvironmentConfigAction: GetEnvironmentConfigAction\n)(implicit ec: ExecutionContext) {\n  private val logger = LoggerFactory.getLogger(this.getClass)\n\n  private def getEnvironmentConfig: Future[Either[ErrorResponse, GetEnvironmentConfig.Response]] = handleRequest {\n    logger.info(\"Get frontend config\")\n    for {\n      response <- getEnvironmentConfigAction()\n    } yield Right(response)\n  }\n\n  def routes: List[ServerEndpoint[PekkoStreams with WebSockets, Future]] = {\n    List(EnvironmentConfigEndpoints.getEnvironmentConfig.serverLogic(_ => getEnvironmentConfig))\n  }\n}\n"
  },
  {
    "path": "server/src/main/scala/controllers/HealthController.scala",
    "content": "package controllers\n\nimport net.wiringbits.api.endpoints.HealthEndpoints\nimport sttp.capabilities.WebSockets\nimport sttp.capabilities.pekko.PekkoStreams\nimport sttp.model.headers.{Cookie, CookieValueWithMeta, CookieWithMeta}\nimport sttp.tapir.server.ServerEndpoint\n\nimport java.time.Instant\nimport java.time.temporal.ChronoUnit\nimport javax.inject.Inject\nimport scala.concurrent.{ExecutionContext, Future}\n\nclass HealthController @Inject() (implicit ec: ExecutionContext) {\n  private def check: Future[Either[Unit, Unit]] =\n    Future.successful(Right(()))\n\n  def routes: List[ServerEndpoint[PekkoStreams with WebSockets, Future]] = {\n    List(HealthEndpoints.check.serverLogic(_ => check))\n  }\n}\n"
  },
  {
    "path": "server/src/main/scala/controllers/UsersController.scala",
    "content": "package controllers\n\nimport net.wiringbits.actions.*\nimport net.wiringbits.actions.users.*\nimport net.wiringbits.api.endpoints.UsersEndpoints\nimport net.wiringbits.api.models.*\nimport net.wiringbits.api.models.users.*\nimport org.slf4j.LoggerFactory\nimport sttp.capabilities.WebSockets\nimport sttp.capabilities.pekko.PekkoStreams\nimport sttp.tapir.server.ServerEndpoint\n\nimport java.util.UUID\nimport javax.inject.Inject\nimport scala.concurrent.{ExecutionContext, Future}\n\nclass UsersController @Inject() (\n    createUserAction: CreateUserAction,\n    verifyUserEmailAction: VerifyUserEmailAction,\n    forgotPasswordAction: ForgotPasswordAction,\n    resetPasswordAction: ResetPasswordAction,\n    updateUserAction: UpdateUserAction,\n    updatePasswordAction: UpdatePasswordAction,\n    getUserLogsAction: GetUserLogsAction,\n    sendEmailVerificationTokenAction: SendEmailVerificationTokenAction\n)(implicit ec: ExecutionContext) {\n  private val logger = LoggerFactory.getLogger(this.getClass)\n\n  private def create(request: CreateUser.Request): Future[Either[ErrorResponse, CreateUser.Response]] = handleRequest {\n    logger.info(s\"Create user: ${request.email.string}\")\n    for {\n      response <- createUserAction(request)\n    } yield Right(response)\n  }\n\n  private def verifyEmail(request: VerifyEmail.Request) = handleRequest {\n    val token = request.token\n    logger.info(s\"Verify user's email: ${token.userId}\")\n    for {\n      response <- verifyUserEmailAction(token.userId, token.token)\n    } yield Right(response)\n  }\n\n  private def forgotPassword(request: ForgotPassword.Request): Future[Either[ErrorResponse, ForgotPassword.Response]] =\n    handleRequest {\n      logger.info(s\"Send a link to reset password for user with email: ${request.email}\")\n      for {\n        response <- forgotPasswordAction(request)\n      } yield Right(response)\n    }\n\n  private def resetPassword(request: ResetPassword.Request): Future[Either[ErrorResponse, ResetPassword.Response]] =\n    handleRequest {\n      logger.info(s\"Reset user's password: ${request.token.userId}\")\n      for {\n        response <- resetPasswordAction(request.token.userId, request.token.token, request.password)\n      } yield Right(response)\n    }\n\n  private def sendEmailVerificationToken(\n      request: SendEmailVerificationToken.Request\n  ): Future[Either[ErrorResponse, SendEmailVerificationToken.Response]] =\n    handleRequest {\n      logger.info(s\"Send email to: ${request.email}\")\n      for {\n        response <- sendEmailVerificationTokenAction(request)\n      } yield Right(response)\n    }\n\n  private def update(\n      request: UpdateUser.Request,\n      userIdF: Future[UUID]\n  ): Future[Either[ErrorResponse, UpdateUser.Response]] = handleRequest {\n    logger.info(s\"Update user: $request\")\n    for {\n      userId <- userIdF\n      _ <- updateUserAction(userId, request)\n      response = UpdateUser.Response()\n    } yield Right(response)\n  }\n\n  private def updatePassword(\n      request: UpdatePassword.Request,\n      userIdF: Future[UUID]\n  ): Future[Either[ErrorResponse, UpdatePassword.Response]] = handleRequest {\n    for {\n      userId <- userIdF\n      _ = logger.info(s\"Update password for: $userId\")\n      _ <- updatePasswordAction(userId, request)\n      response = UpdatePassword.Response()\n    } yield Right(response)\n  }\n\n  private def getLogs(userIdF: Future[UUID]): Future[Either[ErrorResponse, GetUserLogs.Response]] =\n    handleRequest {\n      for {\n        userId <- userIdF\n        _ = logger.info(s\"Get user logs: $userId\")\n        response <- getUserLogsAction(userId)\n      } yield Right(response)\n    }\n\n  def routes: List[ServerEndpoint[PekkoStreams with WebSockets, Future]] = {\n    List(\n      UsersEndpoints.create.serverLogic(create),\n      UsersEndpoints.verifyEmail.serverLogic(verifyEmail),\n      UsersEndpoints.forgotPassword.serverLogic(forgotPassword),\n      UsersEndpoints.resetPassword.serverLogic(resetPassword),\n      UsersEndpoints.sendEmailVerificationToken.serverLogic(sendEmailVerificationToken),\n      UsersEndpoints.update.serverLogic(update),\n      UsersEndpoints.updatePassword.serverLogic(updatePassword),\n      UsersEndpoints.getLogs.serverLogic(getLogs)\n    )\n  }\n}\n"
  },
  {
    "path": "server/src/main/scala/controllers/package.scala",
    "content": "import net.wiringbits.api.models.{ErrorResponse, errorResponseFormat}\nimport org.slf4j.LoggerFactory\nimport play.api.mvc.request.DefaultRequestFactory\nimport play.api.mvc.{CookieHeaderEncoding, RequestHeader, Session}\nimport sttp.tapir.model.ServerRequest\n\nimport java.util.UUID\nimport javax.inject.Inject\nimport scala.concurrent.{ExecutionContext, Future}\nimport scala.language.implicitConversions\nimport scala.util.Try\nimport scala.util.control.NonFatal\n\npackage object controllers {\n  private val logger = LoggerFactory.getLogger(this.getClass)\n\n  class PlayTapirBridge @Inject() (\n      requestFactory: DefaultRequestFactory,\n      cookieHeaderEncoding: CookieHeaderEncoding\n  )(implicit ec: ExecutionContext) {\n    def setSession(userId: UUID): Future[String] = Future {\n      val session = Session(Map(\"id\" -> userId.toString))\n      val playCookie = requestFactory.sessionBaker.encodeAsCookie(session)\n      cookieHeaderEncoding.encodeSetCookieHeader(List(playCookie))\n    }\n\n    def clearSession(): Future[String] = Future {\n      val encoded = requestFactory.sessionBaker.discard.toCookie\n      cookieHeaderEncoding.encodeSetCookieHeader(List(encoded))\n    }\n  }\n\n  def handleRequest[R](\n      block: Future[Right[ErrorResponse, R]]\n  )(implicit ec: ExecutionContext): Future[Either[ErrorResponse, R]] = {\n    block.recover(errorHandler)\n  }\n\n  def errorHandler[R]: PartialFunction[Throwable, Left[ErrorResponse, R]] = {\n    // rendering any error this way should be enough for a while\n    case NonFatal(ex) =>\n      // debug level used because this includes any validation error as well as server errors\n      logger.debug(s\"Error response while handling a request: ${ex.getMessage}\", ex)\n      Left(ErrorResponse(ex.getMessage))\n  }\n\n  // This is the way to access the play request from tapir, we need it to extract the play session\n  // UUID has to be future, because we want to handle the exception in the controllers\n  implicit def authHandler(serverRequest: ServerRequest)(implicit ec: ExecutionContext): Future[UUID] =\n    val session = serverRequest.underlying\n      .asInstanceOf[RequestHeader]\n      .session\n\n    def userIdFromSession = Future {\n      session\n        .get(\"id\")\n        .flatMap(str => Try(UUID.fromString(str)).toOption)\n        .getOrElse(throw new RuntimeException(\"Invalid or missing authentication\"))\n    }\n\n    userIdFromSession\n      .recover { case NonFatal(_) =>\n        throw new RuntimeException(\"Unauthorized: Invalid or missing authentication\")\n      }\n}\n"
  },
  {
    "path": "server/src/main/scala/net/wiringbits/actions/auth/GetUserAction.scala",
    "content": "package net.wiringbits.actions.auth\n\nimport io.scalaland.chimney.dsl.transformInto\nimport net.wiringbits.api.models.auth.GetCurrentUser\nimport net.wiringbits.repositories.UsersRepository\nimport net.wiringbits.repositories.models.User\n\nimport java.util.UUID\nimport javax.inject.Inject\nimport scala.concurrent.{ExecutionContext, Future}\n\nclass GetUserAction @Inject() (\n    usersRepository: UsersRepository\n)(implicit ec: ExecutionContext) {\n\n  def apply(userId: UUID): Future[GetCurrentUser.Response] = {\n    for {\n      user <- unsafeUser(userId)\n    } yield user.transformInto[GetCurrentUser.Response]\n  }\n\n  private def unsafeUser(userId: UUID): Future[User] = {\n    usersRepository\n      .find(userId)\n      .map { maybe =>\n        maybe.getOrElse(\n          throw new RuntimeException(\n            s\"Unexpected error because the user wasn't found: $userId\"\n          )\n        )\n      }\n  }\n}\n"
  },
  {
    "path": "server/src/main/scala/net/wiringbits/actions/auth/LoginAction.scala",
    "content": "package net.wiringbits.actions.auth\n\nimport io.scalaland.chimney.dsl.transformInto\nimport net.wiringbits.api.models.auth.Login\nimport net.wiringbits.apis.ReCaptchaApi\nimport net.wiringbits.repositories.{UserLogsRepository, UsersRepository}\nimport net.wiringbits.validations.{ValidateCaptcha, ValidatePasswordMatches, ValidateVerifiedUser}\n\nimport javax.inject.Inject\nimport scala.concurrent.{ExecutionContext, Future}\n\nclass LoginAction @Inject() (\n    captchaApi: ReCaptchaApi,\n    usersRepository: UsersRepository,\n    userLogsRepository: UserLogsRepository\n)(implicit\n    ec: ExecutionContext\n) {\n  // returns the token to use for authenticating requests\n  def apply(request: Login.Request): Future[Login.Response] = {\n    for {\n      _ <- ValidateCaptcha(captchaApi, request.captcha)\n      // the user is verified\n      maybe <- usersRepository.find(request.email)\n      _ = maybe.foreach(ValidateVerifiedUser.apply)\n\n      // The password matches\n      user = ValidatePasswordMatches(maybe, request.password)\n\n      // A login token is created\n      _ <- userLogsRepository.create(user.id, \"Logged in successfully\")\n    } yield user.transformInto[Login.Response]\n  }\n}\n"
  },
  {
    "path": "server/src/main/scala/net/wiringbits/actions/environmentconfig/GetEnvironmentConfigAction.scala",
    "content": "package net.wiringbits.actions.environmentconfig\n\nimport net.wiringbits.api.models.environmentconfig.GetEnvironmentConfig\nimport net.wiringbits.config.ReCaptchaConfig\n\nimport javax.inject.Inject\nimport scala.concurrent.Future\n\nclass GetEnvironmentConfigAction @Inject() (\n    reCaptchaConfig: ReCaptchaConfig\n)() {\n  def apply(): Future[GetEnvironmentConfig.Response] = Future.successful {\n    GetEnvironmentConfig.Response(reCaptchaConfig.siteKey.string)\n  }\n}\n"
  },
  {
    "path": "server/src/main/scala/net/wiringbits/actions/internal/StreamPendingBackgroundJobsForeverAction.scala",
    "content": "package net.wiringbits.actions.internal\n\nimport org.apache.pekko.actor.ActorSystem\nimport org.apache.pekko.stream.scaladsl.*\nimport net.wiringbits.repositories.BackgroundJobsRepository\nimport net.wiringbits.repositories.models.BackgroundJobData\nimport org.slf4j.LoggerFactory\n\nimport javax.inject.Inject\nimport scala.concurrent.duration.{DurationInt, FiniteDuration}\nimport scala.concurrent.{ExecutionContext, Future}\n\nclass StreamPendingBackgroundJobsForeverAction @Inject() (backgroundJobsRepository: BackgroundJobsRepository)(implicit\n    ec: ExecutionContext,\n    system: ActorSystem\n) {\n  private val logger = LoggerFactory.getLogger(this.getClass)\n\n  def apply(reconnectionDelay: FiniteDuration = 10.seconds): Source[BackgroundJobData, org.apache.pekko.NotUsed] = {\n    // Let's use unfoldAsync to continuously fetch items from database\n    // First execution doesn't involve a delay\n    Source\n      .unfoldAsync[Boolean, Source[BackgroundJobData, Future[Int]]](false) { delay =>\n        logger.trace(s\"Looking for pending background jobs\")\n        org.apache.pekko.pattern\n          .after(if (delay) reconnectionDelay else 0.seconds) {\n            backgroundJobsRepository.streamPendingJobs\n          }\n          .map(source => Some(true -> source))\n      }\n      .flatMapConcat(identity)\n  }\n}\n"
  },
  {
    "path": "server/src/main/scala/net/wiringbits/actions/users/CreateUserAction.scala",
    "content": "package net.wiringbits.actions.users\n\nimport io.scalaland.chimney.dsl.transformInto\nimport net.wiringbits.api.models.users.CreateUser\nimport net.wiringbits.apis.ReCaptchaApi\nimport net.wiringbits.config.UserTokensConfig\nimport net.wiringbits.repositories\nimport net.wiringbits.repositories.UsersRepository\nimport net.wiringbits.repositories.models.User\nimport net.wiringbits.util.{EmailsHelper, TokenGenerator, TokensHelper}\nimport net.wiringbits.validations.{ValidateCaptcha, ValidateEmailIsAvailable}\nimport org.mindrot.jbcrypt.BCrypt\n\nimport java.time.Instant\nimport java.util.UUID\nimport javax.inject.Inject\nimport scala.concurrent.{ExecutionContext, Future}\n\nclass CreateUserAction @Inject() (\n    usersRepository: UsersRepository,\n    reCaptchaApi: ReCaptchaApi,\n    tokenGenerator: TokenGenerator,\n    userTokensConfig: UserTokensConfig,\n    emailsHelper: EmailsHelper\n)(implicit\n    ec: ExecutionContext\n) {\n\n  def apply(request: CreateUser.Request): Future[CreateUser.Response] = {\n    for {\n      _ <- validations(request)\n      hashedPassword = BCrypt.hashpw(request.password.string, BCrypt.gensalt())\n      token = tokenGenerator.next()\n      hmacToken = TokensHelper.doHMACSHA1(token.toString.getBytes(), userTokensConfig.hmacSecret)\n\n      // create the user\n      createUser = repositories.models.User\n        .CreateUser(\n          id = UUID.randomUUID(),\n          name = request.name,\n          email = request.email,\n          hashedPassword = hashedPassword,\n          verifyEmailToken = hmacToken\n        )\n      _ <- usersRepository.create(createUser)\n\n      // then, send the verification email\n      _ <- emailsHelper.sendRegistrationEmailWithVerificationToken(\n        User(\n          id = createUser.id,\n          name = request.name,\n          email = request.email,\n          hashedPassword = hashedPassword,\n          createdAt = Instant.now,\n          verifiedOn = None\n        ),\n        token\n      )\n    } yield createUser.transformInto[CreateUser.Response]\n  }\n\n  private def validations(request: CreateUser.Request) = {\n    for {\n      _ <- ValidateCaptcha(reCaptchaApi, request.captcha)\n      _ <- ValidateEmailIsAvailable(usersRepository, request.email)\n    } yield ()\n  }\n}\n"
  },
  {
    "path": "server/src/main/scala/net/wiringbits/actions/users/ForgotPasswordAction.scala",
    "content": "package net.wiringbits.actions.users\n\nimport net.wiringbits.api.models.users.ForgotPassword\nimport net.wiringbits.apis.ReCaptchaApi\nimport net.wiringbits.repositories.UsersRepository\nimport net.wiringbits.repositories.models.User\nimport net.wiringbits.util.EmailsHelper\nimport net.wiringbits.validations.{ValidateCaptcha, ValidateVerifiedUser}\n\nimport javax.inject.Inject\nimport scala.concurrent.{ExecutionContext, Future}\n\nclass ForgotPasswordAction @Inject() (\n    captchaApi: ReCaptchaApi,\n    usersRepository: UsersRepository,\n    emailsHelper: EmailsHelper\n)(implicit ec: ExecutionContext) {\n  def apply(request: ForgotPassword.Request): Future[ForgotPassword.Response] = {\n    for {\n      _ <- ValidateCaptcha(captchaApi, request.captcha)\n      userMaybe <- usersRepository.find(request.email)\n\n      // submit the email only when the user exists, otherwise, ignore the request\n      _ <- userMaybe.map(whenExists).getOrElse(Future.unit)\n    } yield ForgotPassword.Response()\n  }\n\n  private def whenExists(user: User) = {\n    for {\n      _ <- Future { ValidateVerifiedUser(user) }\n      _ <- emailsHelper.sendPasswordRecoveryEmail(user)\n    } yield ()\n  }\n}\n"
  },
  {
    "path": "server/src/main/scala/net/wiringbits/actions/users/GetUserLogsAction.scala",
    "content": "package net.wiringbits.actions.users\n\nimport io.scalaland.chimney.dsl.transformInto\nimport net.wiringbits.api.models.users.GetUserLogs\nimport net.wiringbits.repositories.UserLogsRepository\n\nimport java.util.UUID\nimport javax.inject.Inject\nimport scala.concurrent.{ExecutionContext, Future}\n\nclass GetUserLogsAction @Inject() (\n    userLogsRepository: UserLogsRepository\n)(implicit ec: ExecutionContext) {\n\n  def apply(userId: UUID): Future[GetUserLogs.Response] = {\n    for {\n      logs <- userLogsRepository.logs(userId)\n      items = logs.map(_.transformInto[GetUserLogs.Response.UserLog])\n    } yield GetUserLogs.Response(items)\n  }\n}\n"
  },
  {
    "path": "server/src/main/scala/net/wiringbits/actions/users/ResetPasswordAction.scala",
    "content": "package net.wiringbits.actions.users\n\nimport net.wiringbits.api.models.users.ResetPassword\nimport net.wiringbits.common.models.Password\nimport net.wiringbits.config.UserTokensConfig\nimport net.wiringbits.repositories.{UserTokensRepository, UsersRepository}\nimport net.wiringbits.util.{EmailMessage, TokensHelper}\nimport net.wiringbits.validations.ValidateUserToken\nimport org.mindrot.jbcrypt.BCrypt\n\nimport java.time.Clock\nimport java.util.UUID\nimport javax.inject.Inject\nimport scala.concurrent.{ExecutionContext, Future}\n\nclass ResetPasswordAction @Inject() (\n    userTokensConfig: UserTokensConfig,\n    usersRepository: UsersRepository,\n    userTokensRepository: UserTokensRepository\n)(implicit\n    ec: ExecutionContext,\n    clock: Clock\n) {\n\n  def apply(userId: UUID, token: UUID, password: Password): Future[ResetPassword.Response] = {\n    val hashedPassword = BCrypt.hashpw(password.string, BCrypt.gensalt())\n    val hmacToken = TokensHelper.doHMACSHA1(token.toString.getBytes, userTokensConfig.hmacSecret)\n    for {\n      // When the token valid\n      tokenMaybe <- userTokensRepository.find(userId, hmacToken)\n      token = tokenMaybe.getOrElse(throw new RuntimeException(s\"Token for user $userId wasn't found\"))\n      _ = ValidateUserToken(token)\n\n      // We trigger the reset password flow\n      userMaybe <- usersRepository.find(userId)\n      user = userMaybe.getOrElse(throw new RuntimeException(s\"User with id $userId wasn't found\"))\n      emailMessage = EmailMessage.resetPassword(user.name)\n      _ <- usersRepository.resetPassword(userId, hashedPassword, emailMessage)\n    } yield ResetPassword.Response(name = user.name, email = user.email)\n  }\n}\n"
  },
  {
    "path": "server/src/main/scala/net/wiringbits/actions/users/SendEmailVerificationTokenAction.scala",
    "content": "package net.wiringbits.actions.users\n\nimport net.wiringbits.api.models.users.SendEmailVerificationToken\nimport net.wiringbits.apis.ReCaptchaApi\nimport net.wiringbits.repositories.UsersRepository\nimport net.wiringbits.util.EmailsHelper\nimport net.wiringbits.validations.{ValidateCaptcha, ValidateEmailIsRegistered, ValidateUserIsNotVerified}\n\nimport javax.inject.Inject\nimport scala.concurrent.{ExecutionContext, Future}\n\nclass SendEmailVerificationTokenAction @Inject() (\n    usersRepository: UsersRepository,\n    emailsHelper: EmailsHelper,\n    reCaptchaApi: ReCaptchaApi\n)(implicit ec: ExecutionContext) {\n\n  def apply(request: SendEmailVerificationToken.Request): Future[SendEmailVerificationToken.Response] = {\n    for {\n      _ <- validations(request)\n      userMaybe <- usersRepository.find(request.email)\n      user = userMaybe.getOrElse(throw new RuntimeException(s\"User with email ${request.email} wasn't found\"))\n      _ = ValidateUserIsNotVerified(user)\n\n      expiresAt <- emailsHelper.sendEmailVerificationToken(user)\n    } yield SendEmailVerificationToken.Response(expiresAt = expiresAt)\n  }\n\n  private def validations(request: SendEmailVerificationToken.Request) = {\n    for {\n      _ <- ValidateCaptcha(reCaptchaApi, request.captcha)\n      _ <- ValidateEmailIsRegistered(usersRepository, request.email)\n    } yield ()\n  }\n}\n"
  },
  {
    "path": "server/src/main/scala/net/wiringbits/actions/users/UpdatePasswordAction.scala",
    "content": "package net.wiringbits.actions.users\n\nimport net.wiringbits.api.models.users.UpdatePassword\nimport net.wiringbits.repositories.UsersRepository\nimport net.wiringbits.util.EmailMessage\nimport net.wiringbits.validations.ValidatePasswordMatches\nimport org.mindrot.jbcrypt.BCrypt\n\nimport java.util.UUID\nimport javax.inject.Inject\nimport scala.concurrent.{ExecutionContext, Future}\n\nclass UpdatePasswordAction @Inject() (\n    usersRepository: UsersRepository\n)(implicit ec: ExecutionContext) {\n\n  def apply(userId: UUID, request: UpdatePassword.Request): Future[Unit] = {\n    for {\n      maybe <- usersRepository.find(userId)\n      user = ValidatePasswordMatches(maybe, request.oldPassword)\n      hashedPassword = BCrypt.hashpw(request.newPassword.string, BCrypt.gensalt())\n      emailMessage = EmailMessage.updatePassword(user.name)\n      _ <- usersRepository.updatePassword(userId, hashedPassword, emailMessage)\n    } yield ()\n  }\n}\n"
  },
  {
    "path": "server/src/main/scala/net/wiringbits/actions/users/UpdateUserAction.scala",
    "content": "package net.wiringbits.actions.users\n\nimport net.wiringbits.api.models.users.UpdateUser\nimport net.wiringbits.repositories.UsersRepository\n\nimport java.util.UUID\nimport javax.inject.Inject\nimport scala.concurrent.{ExecutionContext, Future}\n\nclass UpdateUserAction @Inject() (\n    usersRepository: UsersRepository\n)(implicit ec: ExecutionContext) {\n\n  def apply(userId: UUID, request: UpdateUser.Request): Future[Unit] = {\n    val validate = Future {\n      if (request.name.string.isEmpty) new RuntimeException(s\"The name is required\")\n      else ()\n    }\n\n    for {\n      _ <- validate\n      _ <- usersRepository.update(userId, request.name)\n    } yield ()\n  }\n}\n"
  },
  {
    "path": "server/src/main/scala/net/wiringbits/actions/users/VerifyUserEmailAction.scala",
    "content": "package net.wiringbits.actions.users\n\nimport net.wiringbits.api.models.users.VerifyEmail\nimport net.wiringbits.config.UserTokensConfig\nimport net.wiringbits.repositories.{UserTokensRepository, UsersRepository}\nimport net.wiringbits.util.{EmailMessage, TokensHelper}\nimport net.wiringbits.validations.{ValidateUserIsNotVerified, ValidateUserToken}\n\nimport java.time.Clock\nimport java.util.UUID\nimport javax.inject.Inject\nimport scala.concurrent.{ExecutionContext, Future}\n\nclass VerifyUserEmailAction @Inject() (\n    usersRepository: UsersRepository,\n    userTokensRepository: UserTokensRepository,\n    userTokensConfig: UserTokensConfig\n)(implicit\n    ec: ExecutionContext,\n    clock: Clock\n) {\n  def apply(userId: UUID, token: UUID): Future[VerifyEmail.Response] = for {\n    // when the user is not verified\n    userMaybe <- usersRepository.find(userId)\n    user = userMaybe.getOrElse(throw new RuntimeException(s\"User wasn't found\"))\n    _ = ValidateUserIsNotVerified(user)\n\n    // the token is validated\n    hmacToken = TokensHelper.doHMACSHA1(token.toString.getBytes, userTokensConfig.hmacSecret)\n    tokenMaybe <- userTokensRepository.find(userId, hmacToken)\n    userToken = tokenMaybe.getOrElse(throw new RuntimeException(s\"Token for user $userId wasn't found\"))\n    _ = ValidateUserToken(userToken)\n\n    // then, the user is marked as verified\n    emailMessage = EmailMessage.confirm(user.name)\n    _ <- usersRepository.verify(userId = userId, tokenId = userToken.id, emailMessage)\n  } yield VerifyEmail.Response()\n}\n"
  },
  {
    "path": "server/src/main/scala/net/wiringbits/apis/EmailApi.scala",
    "content": "package net.wiringbits.apis\n\nimport net.wiringbits.apis.models.EmailRequest\nimport org.slf4j.LoggerFactory\n\nimport javax.inject.Inject\nimport scala.concurrent.{ExecutionContext, Future}\n\ntrait EmailApi {\n  def sendEmail(emailRequest: EmailRequest): Future[Unit]\n}\n\nobject EmailApi {\n  class LogImpl @Inject() (implicit ec: ExecutionContext) extends EmailApi {\n    private val logger = LoggerFactory.getLogger(this.getClass)\n\n    override def sendEmail(request: EmailRequest): Future[Unit] = Future {\n      logger.info(\n        s\"Sending email, to = ${request.destination}, subject = ${request.message.subject}, body = ${request.message.body}\"\n      )\n    }\n  }\n}\n"
  },
  {
    "path": "server/src/main/scala/net/wiringbits/apis/EmailApiAWSImpl.scala",
    "content": "package net.wiringbits.apis\n\nimport net.wiringbits.apis.models.EmailRequest\nimport net.wiringbits.config.{AWSConfig, EmailConfig}\nimport org.slf4j.LoggerFactory\nimport software.amazon.awssdk.auth.credentials.{AwsBasicCredentials, StaticCredentialsProvider}\nimport software.amazon.awssdk.services.ses.SesAsyncClient\nimport software.amazon.awssdk.services.ses.model.*\n\nimport javax.inject.Inject\nimport scala.jdk.FutureConverters.CompletionStageOps\nimport scala.concurrent.ExecutionContext.Implicits.global\nimport scala.concurrent.{Future, blocking}\n\nclass EmailApiAWSImpl @Inject() (\n    emailConfig: EmailConfig,\n    awsConfig: AWSConfig\n) extends EmailApi {\n  private val logger = LoggerFactory.getLogger(this.getClass)\n\n  override def sendEmail(emailRequest: EmailRequest): Future[Unit] = {\n\n    val from = emailConfig.senderAddress\n\n    val htmlBody =\n      s\"\"\"<p>${emailRequest.message.body}</p>\"\"\".stripMargin\n\n    def unsafe: Future[Unit] = try {\n      val credentials = AwsBasicCredentials.create(awsConfig.accessKeyId.string, awsConfig.secretAccessKey.string)\n      val credentialsProvider = StaticCredentialsProvider.create(credentials)\n\n      val client = SesAsyncClient.builder.region(awsConfig.region).credentialsProvider(credentialsProvider).build()\n\n      val destination = Destination.builder.toAddresses(emailRequest.destination.string).build()\n      val body = Body.builder\n        .html(Content.builder.charset(\"UTF-8\").data(htmlBody).build())\n        .text(Content.builder.charset(\"UTF-8\").data(emailRequest.message.body).build())\n        .build()\n      val subject = Content.builder.charset(\"UTF-8\").data(emailRequest.message.subject).build\n      val message = Message.builder.body(body).subject(subject).build()\n      val request = SendEmailRequest.builder\n        .source(from)\n        .destination(destination)\n        .message(message)\n        .build()\n      for {\n        response <- blocking {\n          client.sendEmail(request)\n        }.asScala\n        _ = logger.info(\n          s\"Email sent, to: ${emailRequest.destination}, subject = ${emailRequest.message.subject}, messageId = ${response.messageId()}\"\n        )\n      } yield ()\n    } catch {\n      case ex: Exception =>\n        throw new RuntimeException(\n          s\"Email was not sent, to: ${emailRequest.destination}, subject = ${emailRequest.message.subject}\",\n          ex\n        )\n    }\n\n    Future {\n      blocking(unsafe)\n    }.flatten\n  }\n}\n"
  },
  {
    "path": "server/src/main/scala/net/wiringbits/apis/ReCaptchaApi.scala",
    "content": "package net.wiringbits.apis\n\nimport net.wiringbits.common.models.Captcha\nimport net.wiringbits.config.ReCaptchaConfig\nimport play.api.libs.json.Json\nimport play.api.libs.ws.DefaultBodyWritables.writeableOf_String\nimport play.api.libs.ws.WSClient\n\nimport javax.inject.Inject\nimport scala.concurrent.{ExecutionContext, Future}\n\nclass ReCaptchaApi @Inject() (reCaptchaConfig: ReCaptchaConfig, ws: WSClient)(implicit\n    ec: ExecutionContext\n) {\n\n  private val url = \"https://www.google.com/recaptcha/api/siteverify\"\n\n  def verify(captcha: Captcha): Future[Boolean] = {\n    ws.url(url)\n      .addQueryStringParameters(\"secret\" -> reCaptchaConfig.secret.string, \"response\" -> captcha.string)\n      .post(\"{}\")\n      .map { response =>\n        (response.json \\ \"success\")\n          .as[Boolean]\n      }\n  }\n}\n"
  },
  {
    "path": "server/src/main/scala/net/wiringbits/apis/models/EmailRequest.scala",
    "content": "package net.wiringbits.apis.models\n\nimport net.wiringbits.common.models.Email\nimport net.wiringbits.util.EmailMessage\n\ncase class EmailRequest(destination: Email, message: EmailMessage)\n"
  },
  {
    "path": "server/src/main/scala/net/wiringbits/config/AWSConfig.scala",
    "content": "package net.wiringbits.config\n\nimport net.wiringbits.models.{AWSAccessKeyId, AWSSecretAccessKey}\nimport play.api.Configuration\nimport software.amazon.awssdk.regions.Region\n\ncase class AWSConfig(accessKeyId: AWSAccessKeyId, secretAccessKey: AWSSecretAccessKey, region: Region) {\n  override def toString: String = {\n\n    s\"AwsConfig(region = $region, accessKeyId = ${accessKeyId.toString}, secretAccessKey = ${secretAccessKey.toString})\"\n  }\n}\n\nobject AWSConfig {\n  def apply(config: Configuration): AWSConfig = {\n    val accessKeyId = config.get[String](\"accessKeyId\")\n    val secretAccessKey = config.get[String](\"secretAccessKey\")\n    val region = config.get[String](\"region\")\n\n    AWSConfig(AWSAccessKeyId(accessKeyId), AWSSecretAccessKey(secretAccessKey), Region.of(region))\n  }\n}\n"
  },
  {
    "path": "server/src/main/scala/net/wiringbits/config/BackgroundJobsExecutorConfig.scala",
    "content": "package net.wiringbits.config\n\nimport play.api.Configuration\n\nimport scala.concurrent.duration.FiniteDuration\n\ncase class BackgroundJobsExecutorConfig(interval: FiniteDuration) {\n  override def toString: String = {\n    s\"BackgroundJobsExecutorConfig(interval = $interval)\"\n  }\n}\n\nobject BackgroundJobsExecutorConfig {\n  def apply(config: Configuration): BackgroundJobsExecutorConfig = {\n    val interval = config.get[FiniteDuration](\"interval\")\n    BackgroundJobsExecutorConfig(interval)\n  }\n}\n"
  },
  {
    "path": "server/src/main/scala/net/wiringbits/config/EmailConfig.scala",
    "content": "package net.wiringbits.config\n\nimport play.api.Configuration\n\ncase class EmailConfig(senderAddress: String, provider: String) {\n  override def toString: String = {\n    s\"EmailConfig(senderAddress = $senderAddress, provider = $provider)\"\n  }\n}\n\nobject EmailConfig {\n  def apply(config: Configuration): EmailConfig = {\n    val senderAddress = config.get[String](\"senderAddress\")\n    val provider = config.get[String](\"provider\")\n    new EmailConfig(senderAddress = senderAddress, provider = provider)\n  }\n}\n"
  },
  {
    "path": "server/src/main/scala/net/wiringbits/config/ReCaptchaConfig.scala",
    "content": "package net.wiringbits.config\n\nimport net.wiringbits.models.{ReCaptchaSecret, ReCaptchaSiteKey}\nimport play.api.Configuration\n\ncase class ReCaptchaConfig(secret: ReCaptchaSecret, siteKey: ReCaptchaSiteKey) {\n  override def toString: String = {\n\n    s\"ReCaptchaConfig(secret = ${secret.toString}, siteKey = ${siteKey})\"\n  }\n}\n\nobject ReCaptchaConfig {\n  def apply(config: Configuration): ReCaptchaConfig = {\n    val secret = config.get[String](\"secretKey\")\n    val siteKey = config.get[String](\"siteKey\")\n    ReCaptchaConfig(ReCaptchaSecret(secret), ReCaptchaSiteKey(siteKey))\n  }\n}\n"
  },
  {
    "path": "server/src/main/scala/net/wiringbits/config/SwaggerConfig.scala",
    "content": "package net.wiringbits.config\n\nimport play.api.Configuration\n\ncase class SwaggerConfig(basePath: String, info: SwaggerConfig.Info) {\n  override def toString: String = s\"SwaggerConfig($basePath, $info)\"\n}\n\nobject SwaggerConfig {\n  case class Info(version: String, contact: String, title: String, description: String) {\n    override def toString: String = s\"Info($version, $contact, $title, $description)\"\n  }\n\n  def apply(config: Configuration): SwaggerConfig = {\n    val apiConfig = config.get[Configuration](\"api\")\n    val apiInfoConfig = apiConfig.get[Configuration](\"info\")\n\n    val basePath = apiConfig.get[String](\"basePath\")\n    val version = apiInfoConfig.get[String](\"version\")\n    val contact = apiInfoConfig.get[String](\"contact\")\n    val title = apiInfoConfig.get[String](\"title\")\n    val description = apiInfoConfig.get[String](\"description\")\n    SwaggerConfig(basePath, Info(version, contact, title, description))\n  }\n}\n"
  },
  {
    "path": "server/src/main/scala/net/wiringbits/config/UserTokensConfig.scala",
    "content": "package net.wiringbits.config\n\nimport play.api.Configuration\n\nimport scala.concurrent.duration.FiniteDuration\n\ncase class UserTokensConfig(\n    emailVerificationExp: FiniteDuration,\n    resetPasswordExp: FiniteDuration,\n    hmacSecret: String\n) {\n  override def toString: String = {\n    import net.wiringbits.util.StringUtils.Implicits.*\n\n    s\"UserTokensConfig(emailVerificationExp = $emailVerificationExp, resetPasswordExp = $resetPasswordExp, hmacSecret = ${hmacSecret.mask()})\"\n  }\n}\n\nobject UserTokensConfig {\n\n  def apply(conf: Configuration): UserTokensConfig = {\n    val emailVerificationExp = conf.get[FiniteDuration](\"emailVerification.expirationTime\")\n    val resetPasswordExp = conf.get[FiniteDuration](\"resetPassword.expirationTime\")\n    val hmacSecret = conf.get[String](\"hmacSecret\")\n\n    UserTokensConfig(emailVerificationExp, resetPasswordExp, hmacSecret)\n  }\n}\n"
  },
  {
    "path": "server/src/main/scala/net/wiringbits/config/WebAppConfig.scala",
    "content": "package net.wiringbits.config\n\nimport play.api.Configuration\n\ncase class WebAppConfig(host: String) {\n  override def toString: String = {\n    s\"WebAppConfig(host = $host)\"\n  }\n}\n\nobject WebAppConfig {\n  def apply(config: Configuration): WebAppConfig = {\n    val url = config.get[String](\"host\")\n\n    WebAppConfig(url)\n  }\n}\n"
  },
  {
    "path": "server/src/main/scala/net/wiringbits/executors/DatabaseExecutionContext.scala",
    "content": "package net.wiringbits.executors\n\nimport org.apache.pekko.actor.ActorSystem\nimport play.api.libs.concurrent.CustomExecutionContext\n\nimport javax.inject.{Inject, Singleton}\nimport scala.concurrent.ExecutionContext\n\ntrait DatabaseExecutionContext extends ExecutionContext\n\nobject DatabaseExecutionContext {\n\n  @Singleton\n  class AkkaBased @Inject() (system: ActorSystem)\n      extends CustomExecutionContext(system, \"database.dispatcher\")\n      with DatabaseExecutionContext\n}\n"
  },
  {
    "path": "server/src/main/scala/net/wiringbits/models/AWSAccessKeyId.scala",
    "content": "package net.wiringbits.models\n\nimport com.typesafe.config.Config\nimport play.api.ConfigLoader\n\ncase class AWSAccessKeyId(string: String) extends SecretValue(string)\n\nobject AWSAccessKeyId {\n\n  implicit val configLoader: ConfigLoader[AWSAccessKeyId] = (config: Config, path: String) => {\n    AWSAccessKeyId(string = config.getString(path))\n  }\n}\n"
  },
  {
    "path": "server/src/main/scala/net/wiringbits/models/AWSSecretAccessKey.scala",
    "content": "package net.wiringbits.models\n\nimport com.typesafe.config.Config\nimport play.api.ConfigLoader\n\ncase class AWSSecretAccessKey(string: String) extends SecretValue(string)\n\nobject AWSSecretAccessKey {\n\n  implicit val configLoader: ConfigLoader[AWSSecretAccessKey] = (config: Config, path: String) => {\n    AWSSecretAccessKey(string = config.getString(path))\n  }\n}\n"
  },
  {
    "path": "server/src/main/scala/net/wiringbits/models/ReCaptchaSecret.scala",
    "content": "package net.wiringbits.models\n\nimport com.typesafe.config.Config\nimport play.api.ConfigLoader\n\ncase class ReCaptchaSecret(string: String) extends SecretValue(string)\n\nobject ReCaptchaSecret {\n\n  implicit val configLoader: ConfigLoader[ReCaptchaSecret] = (config: Config, path: String) => {\n    ReCaptchaSecret(string = config.getString(path))\n  }\n}\n"
  },
  {
    "path": "server/src/main/scala/net/wiringbits/models/ReCaptchaSiteKey.scala",
    "content": "package net.wiringbits.models\n\nimport com.typesafe.config.Config\nimport play.api.ConfigLoader\n\ncase class ReCaptchaSiteKey(string: String)\n\nobject ReCaptchaSiteKey {\n  implicit val configLoader: ConfigLoader[ReCaptchaSiteKey] = (config: Config, path: String) => {\n    ReCaptchaSiteKey(string = config.getString(path))\n  }\n}\n"
  },
  {
    "path": "server/src/main/scala/net/wiringbits/models/SecretValue.scala",
    "content": "package net.wiringbits.models\n\nimport net.wiringbits.util.StringUtils.Implicits.StringUtilsExt\n\nabstract class SecretValue(string: String) {\n  override def toString: String = string.mask()\n}\n"
  },
  {
    "path": "server/src/main/scala/net/wiringbits/models/jobs/BackgroundJobPayload.scala",
    "content": "package net.wiringbits.models.jobs\n\nimport net.wiringbits.common.models.Email\nimport play.api.libs.json.{Format, Json, Writes}\n\nsealed trait BackgroundJobPayload extends Product with Serializable\n\n/** NOTE: Updating these models can cause tasks to fail, for example, adding an extra argument to SendEmail would cause\n  * the json parsing to fail when we already have jobs in the database\n  */\nobject BackgroundJobPayload {\n  case class SendEmail(email: Email, subject: String, body: String) extends BackgroundJobPayload\n\n  object SendEmail {\n    implicit val sendEmailFormat: Format[SendEmail] = Json.format\n  }\n\n  implicit val backgroundJobPayloadWrites: Writes[BackgroundJobPayload] = { case payload: SendEmail =>\n    Json.toJson(payload)\n  }\n}\n"
  },
  {
    "path": "server/src/main/scala/net/wiringbits/models/jobs/BackgroundJobStatus.scala",
    "content": "package net.wiringbits.models.jobs\n\nimport enumeratum.EnumEntry.Uppercase\nimport enumeratum.{Enum, EnumEntry}\n\nsealed trait BackgroundJobStatus extends EnumEntry with Uppercase\n\nobject BackgroundJobStatus extends Enum[BackgroundJobStatus] {\n  case object Success extends BackgroundJobStatus\n  case object Pending extends BackgroundJobStatus\n  case object Failed extends BackgroundJobStatus\n\n  val values = findValues\n}\n"
  },
  {
    "path": "server/src/main/scala/net/wiringbits/models/jobs/BackgroundJobType.scala",
    "content": "package net.wiringbits.models.jobs\n\nimport enumeratum.EnumEntry.Uppercase\nimport enumeratum.{Enum, EnumEntry}\n\nsealed trait BackgroundJobType extends EnumEntry with Uppercase\n\n/** NOTE: Updating this model can cause tasks to fail, for example, if SendEmail is removed while there are pending\n  * SendEmail tasks stored at the database\n  */\nobject BackgroundJobType extends Enum[BackgroundJobType] {\n  case object SendEmail extends BackgroundJobType\n\n  val values = findValues\n}\n"
  },
  {
    "path": "server/src/main/scala/net/wiringbits/modules/ApisModule.scala",
    "content": "package net.wiringbits.modules\n\nimport com.google.inject.{AbstractModule, Provider}\nimport net.wiringbits.apis.{EmailApi, EmailApiAWSImpl}\nimport net.wiringbits.config.EmailConfig\nimport org.slf4j.LoggerFactory\n\nimport javax.inject.Inject\n\nclass ApisModule extends AbstractModule {\n  override def configure(): Unit = {\n    val _ = bind(classOf[EmailApi])\n      .toProvider(classOf[ApisModule.EmailApiProvider])\n      .asEagerSingleton()\n  }\n}\n\nobject ApisModule {\n\n  class EmailApiProvider @Inject() (config: EmailConfig, logImpl: EmailApi.LogImpl, awsImpl: EmailApiAWSImpl)\n      extends Provider[EmailApi] {\n\n    private val logger = LoggerFactory.getLogger(this.getClass)\n\n    override def get(): EmailApi = {\n      if (config.provider equalsIgnoreCase \"aws\") {\n        logger.info(\"Mail provider set to AWS\")\n        awsImpl\n      } else {\n        logger.info(\"Mail provider set to none, emails will be printed as logs\")\n        logImpl\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "server/src/main/scala/net/wiringbits/modules/ClockModule.scala",
    "content": "package net.wiringbits.modules\n\nimport com.google.inject.AbstractModule\n\nimport java.time.Clock\n\nclass ClockModule extends AbstractModule {\n\n  override def configure(): Unit = {\n    bind(classOf[Clock]).toInstance(Clock.systemUTC())\n  }\n}\n"
  },
  {
    "path": "server/src/main/scala/net/wiringbits/modules/ConfigModule.scala",
    "content": "package net.wiringbits.modules\n\nimport com.google.inject.{AbstractModule, Provides}\nimport net.wiringbits.config.*\nimport org.slf4j.LoggerFactory\nimport play.api.Configuration\n\nimport javax.inject.Singleton\n\nclass ConfigModule extends AbstractModule {\n\n  private val logger = LoggerFactory.getLogger(this.getClass)\n\n  @Provides\n  @Singleton\n  def recaptchaConfig(global: Configuration): ReCaptchaConfig = {\n    val config = ReCaptchaConfig(global.get[Configuration](\"recaptcha\"))\n    logger.info(s\"Config loaded: $config\")\n    config\n  }\n\n  @Provides\n  @Singleton\n  def emailConfig(global: Configuration): EmailConfig = {\n    val config = EmailConfig(global.get[Configuration](\"email\"))\n    logger.info(s\"Config loaded: $config\")\n    config\n  }\n\n  @Provides\n  @Singleton\n  def webAppConfig(global: Configuration): WebAppConfig = {\n    val config = WebAppConfig(global.get[Configuration](\"webapp\"))\n    logger.info(s\"Config loaded: $config\")\n    config\n  }\n\n  @Provides\n  @Singleton\n  def userTokensConfig(global: Configuration): UserTokensConfig = {\n    val config = UserTokensConfig(global.get[Configuration](\"userTokens\"))\n    logger.info(s\"Config loaded: $config\")\n    config\n  }\n\n  @Provides\n  @Singleton\n  def awsConfig(global: Configuration): AWSConfig = {\n    val config = AWSConfig(global.get[Configuration](\"aws\"))\n    logger.info(s\"Config loaded: $config\")\n    config\n  }\n\n  @Provides\n  @Singleton\n  def backgroundJobsExecutorConfig(global: Configuration): BackgroundJobsExecutorConfig = {\n    val config = BackgroundJobsExecutorConfig(global.get[Configuration](\"backgroundJobsExecutorTask\"))\n    logger.info(s\"Config loaded: $config\")\n    config\n  }\n\n  @Provides\n  @Singleton\n  def swaggerConfig(global: Configuration): SwaggerConfig = {\n    val config = SwaggerConfig(global.get[Configuration](\"swagger\"))\n    logger.info(s\"Config loaded: $config\")\n    config\n  }\n}\n"
  },
  {
    "path": "server/src/main/scala/net/wiringbits/modules/ExecutorsModule.scala",
    "content": "package net.wiringbits.modules\n\nimport com.google.inject.AbstractModule\nimport net.wiringbits.executors.DatabaseExecutionContext\n\nclass ExecutorsModule extends AbstractModule {\n\n  override def configure(): Unit = {\n    val _ = bind(classOf[DatabaseExecutionContext]).to(classOf[DatabaseExecutionContext.AkkaBased]).asEagerSingleton()\n  }\n}\n"
  },
  {
    "path": "server/src/main/scala/net/wiringbits/modules/TasksModule.scala",
    "content": "package net.wiringbits.modules\n\nimport com.google.inject.AbstractModule\nimport net.wiringbits.tasks.BackgroundJobsExecutorTask\n\nclass TasksModule extends AbstractModule {\n\n  override def configure(): Unit = {\n    bind(classOf[BackgroundJobsExecutorTask]).asEagerSingleton()\n  }\n}\n"
  },
  {
    "path": "server/src/main/scala/net/wiringbits/repositories/BackgroundJobsRepository.scala",
    "content": "package net.wiringbits.repositories\n\nimport net.wiringbits.executors.DatabaseExecutionContext\nimport net.wiringbits.repositories.daos.BackgroundJobDAO\nimport net.wiringbits.repositories.models.BackgroundJobData\nimport play.api.db.Database\n\nimport java.time.{Clock, Instant}\nimport java.util.UUID\nimport javax.inject.Inject\nimport scala.concurrent.Future\nimport scala.util.control.NonFatal\n\nclass BackgroundJobsRepository @Inject() (database: Database)(implicit ec: DatabaseExecutionContext, clock: Clock) {\n  def streamPendingJobs: Future[org.apache.pekko.stream.scaladsl.Source[BackgroundJobData, Future[Int]]] = Future {\n    // autocommit=false is necessary to avoid loading the whole result into memory\n    implicit val conn = database.getConnection(autocommit = false)\n    try {\n      val stream = BackgroundJobDAO.streamPendingJobs()\n\n      // make sure to close the connection when it isn't required anymore\n      stream.mapMaterializedValue { result =>\n        result.onComplete { t =>\n          conn.close()\n          t\n        }\n        result\n      }\n    } catch {\n      case NonFatal(ex) =>\n        conn.close()\n        throw new RuntimeException(\"Failed to stream pending background jobs\", ex)\n    }\n  }\n\n  def setStatusToFailed(backgroundJobId: UUID, executeAt: Instant, failReason: String): Future[Unit] = Future {\n    database.withConnection { implicit conn =>\n      BackgroundJobDAO.setStatusToFailed(backgroundJobId, executeAt, failReason)\n    }\n  }\n\n  def setStatusToSuccess(backgroundJobId: UUID): Future[Unit] = Future {\n    database.withConnection { implicit conn =>\n      BackgroundJobDAO.setStatusToSuccess(backgroundJobId)\n    }\n  }\n}\n"
  },
  {
    "path": "server/src/main/scala/net/wiringbits/repositories/UserLogsRepository.scala",
    "content": "package net.wiringbits.repositories\n\nimport net.wiringbits.executors.DatabaseExecutionContext\nimport net.wiringbits.repositories.daos.UserLogsDAO\nimport net.wiringbits.repositories.models.UserLog\nimport play.api.db.Database\n\nimport java.util.UUID\nimport javax.inject.Inject\nimport scala.concurrent.Future\n\nclass UserLogsRepository @Inject() (database: Database)(implicit ec: DatabaseExecutionContext) {\n\n  def create(request: UserLog.CreateUserLog): Future[Unit] = Future {\n    database.withConnection { implicit conn =>\n      UserLogsDAO.create(request)\n    }\n  }\n\n  def create(userId: UUID, message: String): Future[Unit] = Future {\n    database.withConnection { implicit conn =>\n      val request = UserLog.CreateUserLog(UUID.randomUUID(), userId, message)\n      UserLogsDAO.create(request)\n    }\n  }\n\n  def logs(userId: UUID): Future[List[UserLog]] = Future {\n    database.withConnection { implicit conn =>\n      UserLogsDAO.logs(userId)\n    }\n  }\n}\n"
  },
  {
    "path": "server/src/main/scala/net/wiringbits/repositories/UserTokensRepository.scala",
    "content": "package net.wiringbits.repositories\n\nimport net.wiringbits.executors.DatabaseExecutionContext\nimport net.wiringbits.repositories.daos.UserTokensDAO\nimport net.wiringbits.repositories.models.UserToken\nimport play.api.db.Database\n\nimport java.util.UUID\nimport javax.inject.Inject\nimport scala.concurrent.Future\n\nclass UserTokensRepository @Inject() (\n    database: Database\n)(implicit\n    ec: DatabaseExecutionContext\n) {\n\n  def create(request: UserToken.Create): Future[Unit] = Future {\n    database.withConnection { implicit conn =>\n      UserTokensDAO.create(request)\n    }\n  }\n\n  def find(userId: UUID, token: String): Future[Option[UserToken]] = Future {\n    database.withConnection { implicit conn =>\n      UserTokensDAO.find(userId, token)\n    }\n  }\n\n  def find(userId: UUID): Future[List[UserToken]] = Future {\n    database.withConnection { implicit conn =>\n      UserTokensDAO.find(userId)\n    }\n  }\n\n  def delete(tokenId: UUID, userId: UUID): Future[Unit] = Future {\n    database.withConnection { implicit conn =>\n      UserTokensDAO.delete(tokenId, userId: UUID)\n    }\n  }\n}\n"
  },
  {
    "path": "server/src/main/scala/net/wiringbits/repositories/UsersRepository.scala",
    "content": "package net.wiringbits.repositories\n\nimport net.wiringbits.common.models.{Email, Name}\nimport net.wiringbits.config.UserTokensConfig\nimport net.wiringbits.executors.DatabaseExecutionContext\nimport net.wiringbits.models.jobs.{BackgroundJobPayload, BackgroundJobStatus, BackgroundJobType}\nimport net.wiringbits.repositories.daos.{BackgroundJobDAO, UserLogsDAO, UserTokensDAO, UsersDAO}\nimport net.wiringbits.repositories.models.*\nimport net.wiringbits.util.EmailMessage\nimport play.api.db.Database\n\nimport java.sql.Connection\nimport java.time.Clock\nimport java.time.temporal.ChronoUnit\nimport java.util.UUID\nimport javax.inject.Inject\nimport scala.concurrent.Future\n\nclass UsersRepository @Inject() (\n    database: Database,\n    userTokensConfig: UserTokensConfig\n)(implicit\n    ec: DatabaseExecutionContext,\n    clock: Clock\n) {\n\n  def create(request: User.CreateUser): Future[Unit] = Future {\n    val createToken = UserToken.Create(\n      id = UUID.randomUUID(),\n      token = request.verifyEmailToken,\n      tokenType = UserTokenType.EmailVerification,\n      createdAt = clock.instant(),\n      expiresAt = clock.instant().plus(userTokensConfig.emailVerificationExp.toHours, ChronoUnit.HOURS),\n      userId = request.id\n    )\n\n    database.withTransaction { implicit conn =>\n      UsersDAO.create(request)\n      UserTokensDAO.create(createToken)\n      UserLogsDAO.create(\n        UserLog.CreateUserLog(\n          UUID.randomUUID(),\n          request.id,\n          s\"Account created, name = ${request.name}, email = ${request.email}\"\n        )\n      )\n    }\n  }\n\n  def all(): Future[List[User]] = Future {\n    database.withConnection { implicit conn =>\n      UsersDAO.all()\n    }\n  }\n\n  def find(email: Email): Future[Option[User]] = Future {\n    database.withConnection { implicit conn =>\n      UsersDAO.find(email)\n    }\n  }\n\n  def find(userId: UUID): Future[Option[User]] = Future {\n    database.withConnection { implicit conn =>\n      UsersDAO.find(userId)\n    }\n  }\n\n  def update(userId: UUID, name: Name): Future[Unit] = Future {\n    database.withTransaction { implicit conn =>\n      UsersDAO.updateName(userId, name)\n      UserLogsDAO.create(\n        UserLog.CreateUserLog(\n          UUID.randomUUID(),\n          userId = userId,\n          \"Profile updated\"\n        )\n      )\n    }\n  }\n\n  def updatePassword(userId: UUID, password: String, emailMessage: EmailMessage): Future[Unit] = Future {\n    database.withTransaction { implicit conn =>\n      UsersDAO.resetPassword(userId, password)\n      val request = UserLog.CreateUserLog(UUID.randomUUID(), userId, \"Password was updated\")\n      UserLogsDAO.create(request)\n      sendEmailLater(userId, emailMessage)\n    }\n  }\n\n  def verify(userId: UUID, tokenId: UUID, emailMessage: EmailMessage): Future[Unit] = Future {\n    database.withTransaction { implicit conn =>\n      UsersDAO.verify(userId)\n      UserLogsDAO.create(\n        UserLog.CreateUserLog(\n          UUID.randomUUID(),\n          userId = userId,\n          \"Email verified\"\n        )\n      )\n      UserTokensDAO.delete(tokenId = tokenId, userId = userId)\n      sendEmailLater(userId, emailMessage)\n    }\n  }\n\n  def resetPassword(userId: UUID, password: String, emailMessage: EmailMessage): Future[Unit] = Future {\n    database.withTransaction { implicit conn =>\n      UsersDAO.resetPassword(userId, password)\n      val request = UserLog.CreateUserLog(UUID.randomUUID(), userId, \"Password was reset\")\n      UserLogsDAO.create(request)\n      sendEmailLater(userId, emailMessage)\n    }\n  }\n\n  private def sendEmailLater(userId: UUID, emailMessage: EmailMessage)(implicit conn: Connection): Unit = {\n    val userOpt = UsersDAO.find(userId)\n    userOpt.foreach { user =>\n      val payload = BackgroundJobPayload.SendEmail(\n        email = user.email,\n        subject = emailMessage.subject,\n        body = emailMessage.body\n      )\n      val createNotification = BackgroundJobData.Create(\n        id = UUID.randomUUID(),\n        `type` = BackgroundJobType.SendEmail,\n        payload = payload,\n        status = BackgroundJobStatus.Pending,\n        executeAt = clock.instant(),\n        createdAt = clock.instant(),\n        updatedAt = clock.instant()\n      )\n\n      BackgroundJobDAO.create(createNotification)\n    }\n  }\n}\n"
  },
  {
    "path": "server/src/main/scala/net/wiringbits/repositories/daos/BackgroundJobDAO.scala",
    "content": "package net.wiringbits.repositories.daos\n\nimport anorm.postgresql.*\nimport net.wiringbits.models.jobs.BackgroundJobStatus\nimport net.wiringbits.repositories.models.BackgroundJobData\nimport play.api.libs.json.Json\n\nimport java.sql.Connection\nimport java.time.{Clock, Instant}\nimport java.util.UUID\nimport scala.concurrent.Future\n\nobject BackgroundJobDAO {\n\n  import anorm.*\n\n  def create(request: BackgroundJobData.Create)(implicit conn: Connection): Unit = {\n    val _ = SQL\"\"\"\n      INSERT INTO background_jobs\n        (background_job_id, type, payload, status, execute_at, created_at, updated_at)\n      VALUES (\n        ${request.id},\n        ${request.`type`.toString},\n        ${Json.toJson(request.payload)},\n        ${request.status.toString},\n        ${request.executeAt},\n        ${request.createdAt},\n        ${request.updatedAt}\n      )\n      \"\"\"\n      .execute()\n  }\n\n  def streamPendingJobs(\n      allowedErrors: Int = 10,\n      fetchSize: Int = 1000\n  )(implicit\n      conn: Connection,\n      clock: Clock\n  ): org.apache.pekko.stream.scaladsl.Source[BackgroundJobData, Future[Int]] = {\n    val query = SQL\"\"\"\n      SELECT background_job_id, type, payload, status, status_details, error_count, execute_at, created_at, updated_at\n      FROM background_jobs\n      WHERE status != ${BackgroundJobStatus.Success.toString}\n        AND execute_at <= ${clock.instant()}\n        AND error_count < $allowedErrors\n      ORDER BY execute_at, background_job_id\n      \"\"\".withFetchSize(Some(fetchSize)) // without this, all data is loaded into memory\n\n    PekkoStream.source(query, backgroundJobParser)(conn)\n  }\n\n  def setStatusToFailed(backgroundJobId: UUID, executeAt: Instant, failReason: String)(implicit\n      conn: Connection\n  ): Unit = {\n    val _ = SQL\"\"\"\n      UPDATE background_jobs SET\n        status = ${BackgroundJobStatus.Failed.toString}::TEXT,\n        status_details = $failReason,\n        error_count = error_count + 1,\n        execute_at = $executeAt::TIMESTAMPTZ,\n        updated_at = ${Instant.now()}::TIMESTAMPTZ\n      WHERE background_job_id = ${backgroundJobId.toString}::UUID\n      \"\"\"\n      .execute()\n  }\n\n  def setStatusToSuccess(backgroundJobId: UUID)(implicit\n      conn: Connection\n  ): Unit = {\n    val _ = SQL\"\"\"\n      UPDATE background_jobs SET\n        status = ${BackgroundJobStatus.Success.toString}::TEXT,\n        updated_at = ${Instant.now()}\n      WHERE background_job_id = ${backgroundJobId.toString}::UUID\n      \"\"\"\n      .execute()\n  }\n}\n"
  },
  {
    "path": "server/src/main/scala/net/wiringbits/repositories/daos/UserLogsDAO.scala",
    "content": "package net.wiringbits.repositories.daos\n\nimport net.wiringbits.repositories.models.UserLog\n\nimport java.sql.Connection\nimport java.util.UUID\n\nobject UserLogsDAO {\n\n  import anorm.*\n\n  def create(request: UserLog.CreateUserLog)(implicit conn: Connection): Unit = {\n    val _ = SQL\"\"\"\n        INSERT INTO user_logs\n          (user_log_id, user_id, message, created_at)\n        VALUES (\n          ${request.userLogId.toString}::UUID,\n          ${request.userId.toString}::UUID,\n          ${request.message},\n          NOW()\n        )\n        \"\"\"\n      .execute()\n  }\n\n  def logs(userId: UUID)(implicit conn: Connection): List[UserLog] = {\n    SQL\"\"\"\n      SELECT user_log_id, user_id, message, created_at\n      FROM user_logs\n      WHERE user_id = ${userId.toString}::UUID\n      ORDER BY created_at DESC, user_log_id\n  \"\"\".as(userLogParser.*)\n  }\n}\n"
  },
  {
    "path": "server/src/main/scala/net/wiringbits/repositories/daos/UserTokensDAO.scala",
    "content": "package net.wiringbits.repositories.daos\n\nimport anorm.SqlStringInterpolation\nimport net.wiringbits.repositories.models.UserToken\n\nimport java.sql.Connection\nimport java.util.UUID\n\nobject UserTokensDAO {\n\n  def create(request: UserToken.Create)(implicit\n      conn: Connection\n  ): Unit = {\n    val _ = SQL\"\"\"\n        INSERT INTO user_tokens\n          (user_token_id, token, token_type, created_at, expires_at, user_id)\n        VALUES (\n          ${request.id.toString}::UUID,\n          ${request.token}::TEXT,\n          ${request.tokenType.toString}::TEXT,\n          ${request.createdAt}::TIMESTAMPTZ,\n          ${request.expiresAt}::TIMESTAMPTZ,\n          ${request.userId.toString}::UUID\n        )\n        \"\"\"\n      .execute()\n  }\n\n  def find(userId: UUID, token: String)(implicit conn: Connection): Option[UserToken] = {\n    SQL\"\"\"\n        SELECT user_token_id, token, token_type, created_at, expires_at, user_id\n        FROM user_tokens\n        WHERE user_id = ${userId.toString}::UUID\n          AND token = $token::TEXT\n        \"\"\".as(tokenParser.singleOpt)\n  }\n\n  def find(userId: UUID)(implicit conn: Connection): List[UserToken] = {\n    SQL\"\"\"\n        SELECT user_token_id, token, token_type, created_at, expires_at, user_id\n        FROM user_tokens\n        WHERE user_id = ${userId.toString}::UUID\n        ORDER BY created_at DESC, user_token_id\n       \"\"\".as(tokenParser.*)\n  }\n\n  def delete(tokenId: UUID, userId: UUID)(implicit conn: Connection): Unit = {\n    val _ = SQL\"\"\"\n        DELETE FROM user_tokens\n        WHERE user_id = ${userId.toString}::UUID\n         AND user_token_id = ${tokenId.toString}::UUID\n       \"\"\"\n      .executeUpdate()\n  }\n}\n"
  },
  {
    "path": "server/src/main/scala/net/wiringbits/repositories/daos/UsersDAO.scala",
    "content": "package net.wiringbits.repositories.daos\n\nimport net.wiringbits.common.models.{Email, Name}\nimport net.wiringbits.repositories.models.User\n\nimport java.sql.Connection\nimport java.util.UUID\n\nobject UsersDAO {\n\n  import anorm.*\n\n  def create(request: User.CreateUser)(implicit conn: Connection): Unit = {\n    val _ = SQL\"\"\"\n        INSERT INTO users\n          (user_id, name, email, password, created_at)\n        VALUES (\n          ${request.id.toString}::UUID,\n          ${request.name.string},\n          ${request.email.string},\n          ${request.hashedPassword},\n          NOW()\n        )\n        \"\"\"\n      .execute()\n  }\n\n  def all()(implicit conn: Connection): List[User] = {\n    SQL\"\"\"\n        SELECT user_id, name, email, password, created_at, verified_on\n        FROM users\n        \"\"\".as(userParser.*)\n  }\n\n  def find(email: Email)(implicit conn: Connection): Option[User] = {\n    SQL\"\"\"\n        SELECT user_id, name, email, password, created_at, verified_on\n        FROM users\n        WHERE email = ${email.string}::CITEXT\n        \"\"\".as(userParser.singleOpt)\n  }\n\n  def find(userId: UUID)(implicit conn: Connection): Option[User] = {\n    SQL\"\"\"\n        SELECT user_id, name, email, password, created_at, verified_on\n        FROM users\n        WHERE user_id = ${userId.toString}::UUID\n        \"\"\".as(userParser.singleOpt)\n  }\n\n  def updateName(userId: UUID, name: Name)(implicit conn: Connection): Unit = {\n    val _ = SQL\"\"\"\n      UPDATE users\n      SET name = ${name.string}\n      WHERE user_id = ${userId.toString}::UUID\n    \"\"\".execute()\n  }\n\n  def verify(userId: UUID)(implicit conn: Connection): Unit = {\n    val _ = SQL\"\"\"\n      UPDATE users\n      SET verified_on = NOW()\n      WHERE user_id = ${userId.toString}::UUID\n    \"\"\".execute()\n  }\n\n  def resetPassword(userId: UUID, password: String)(implicit conn: Connection): Unit = {\n    val _ = SQL\"\"\"\n      UPDATE users\n      SET password = $password\n      WHERE user_id = ${userId.toString}::UUID\n    \"\"\".execute()\n  }\n\n  def findUserForUpdate(userId: UUID)(implicit conn: Connection): Option[User] = {\n    SQL\"\"\"\n        SELECT user_id, name, email, password, created_at, verified_on\n        FROM users\n        WHERE user_id = ${userId.toString}::UUID\n        FOR UPDATE NOWAIT\n        \"\"\".as(userParser.singleOpt)\n  }\n}\n"
  },
  {
    "path": "server/src/main/scala/net/wiringbits/repositories/daos/package.scala",
    "content": "package net.wiringbits.repositories\n\nimport anorm.*\nimport anorm.SqlParser.*\nimport anorm.postgresql.*\nimport net.wiringbits.common.models.{Email, Name}\nimport net.wiringbits.models.jobs.{BackgroundJobStatus, BackgroundJobType}\nimport net.wiringbits.repositories.models.*\n\nimport java.time.Instant\nimport java.util.UUID\n\npackage object daos {\n\n  import anorm.{Column, MetaDataItem, TypeDoesNotMatch}\n  import org.postgresql.util.PGobject\n\n  implicit val citextToString: Column[String] = Column.nonNull { case (value, meta) =>\n    val MetaDataItem(qualified, _, clazz) = meta\n    value match {\n      case str: String => Right(str)\n      case obj: PGobject if \"citext\" equalsIgnoreCase obj.getType => Right(obj.getValue)\n      case _ =>\n        Left(\n          TypeDoesNotMatch(\n            s\"Cannot convert $value: ${value.asInstanceOf[AnyRef].getClass} to String for column $qualified, class = $clazz\"\n          )\n        )\n    }\n  }\n\n  implicit val nameParser: Column[Name] = Column.columnToString.map(Name.trusted)\n  implicit val emailParser: Column[Email] = citextToString.map(Email.trusted)\n\n  val userParser: RowParser[User] = {\n    Macro.parser[User](\n      \"user_id\",\n      \"name\",\n      \"email\",\n      \"password\",\n      \"created_at\",\n      \"verified_on\"\n    )\n  }\n\n  val userLogParser: RowParser[UserLog] = {\n    Macro.parser[UserLog](\"user_log_id\", \"user_id\", \"message\", \"created_at\")\n  }\n\n  def enumColumn[A](f: String => Option[A]): Column[A] = Column.columnToString.mapResult { string =>\n    f(string)\n      .toRight(SqlRequestError(new RuntimeException(s\"The value $string doesn't exists\")))\n  }\n\n  implicit val tokenTypeColumn: Column[UserTokenType] = enumColumn(\n    UserTokenType.withNameInsensitiveOption\n  )\n\n  // TODO: Use Macro.parser, for some reason it doesn't work so we have to parse it manually\n  implicit val tokenParser: RowParser[UserToken] = {\n    get[UUID](\"user_token_id\") ~\n      str(\"token\") ~\n      get[UserTokenType](\"token_type\") ~\n      get[Instant](\"created_at\") ~\n      get[Instant](\"expires_at\") ~\n      get[UUID](\"user_id\") map { case tokenId ~ token ~ tokenType ~ createdAt ~ expiresAt ~ userId =>\n        UserToken(\n          id = tokenId,\n          tokenType = tokenType,\n          token = token,\n          createdAt = createdAt,\n          expiresAt = expiresAt,\n          userId = userId\n        )\n      }\n  }\n\n  implicit val backgroundJobStatusColumn: Column[BackgroundJobStatus] = enumColumn(\n    BackgroundJobStatus.withNameInsensitiveOption\n  )\n\n  implicit val backgroundJobTypeColumn: Column[BackgroundJobType] = enumColumn(\n    BackgroundJobType.withNameInsensitiveOption\n  )\n\n  implicit val backgroundJobParser: RowParser[BackgroundJobData] = {\n    Macro.parser[BackgroundJobData](\n      \"background_job_id\",\n      \"type\",\n      \"payload\",\n      \"status\",\n      \"status_details\",\n      \"error_count\",\n      \"execute_at\",\n      \"created_at\",\n      \"updated_at\"\n    )\n  }\n}\n"
  },
  {
    "path": "server/src/main/scala/net/wiringbits/repositories/models/BackgroundJobData.scala",
    "content": "package net.wiringbits.repositories.models\n\nimport net.wiringbits.models.jobs.{BackgroundJobPayload, BackgroundJobStatus, BackgroundJobType}\nimport play.api.libs.json.JsValue\n\nimport java.time.Instant\nimport java.util.UUID\n\ncase class BackgroundJobData(\n    id: UUID,\n    `type`: BackgroundJobType,\n    payload: JsValue,\n    status: BackgroundJobStatus,\n    statusDetails: Option[String],\n    errorCount: Int,\n    executeAt: Instant,\n    createdAt: Instant,\n    updatedAt: Instant\n)\n\nobject BackgroundJobData {\n  case class Create(\n      id: UUID,\n      `type`: BackgroundJobType,\n      payload: BackgroundJobPayload,\n      status: BackgroundJobStatus,\n      executeAt: Instant,\n      createdAt: Instant,\n      updatedAt: Instant\n  )\n}\n"
  },
  {
    "path": "server/src/main/scala/net/wiringbits/repositories/models/User.scala",
    "content": "package net.wiringbits.repositories.models\n\nimport net.wiringbits.common.models.{Email, Name}\n\nimport java.time.Instant\nimport java.util.UUID\n\ncase class User(\n    id: UUID,\n    name: Name,\n    email: Email,\n    hashedPassword: String,\n    createdAt: Instant,\n    verifiedOn: Option[Instant]\n)\n\nobject User {\n  case class CreateUser(id: UUID, name: Name, email: Email, hashedPassword: String, verifyEmailToken: String)\n}\n"
  },
  {
    "path": "server/src/main/scala/net/wiringbits/repositories/models/UserLog.scala",
    "content": "package net.wiringbits.repositories.models\n\nimport java.time.Instant\nimport java.util.UUID\n\ncase class UserLog(userLogId: UUID, userId: UUID, message: String, createdAt: Instant)\n\nobject UserLog {\n  case class CreateUserLog(userLogId: UUID, userId: UUID, message: String)\n}\n"
  },
  {
    "path": "server/src/main/scala/net/wiringbits/repositories/models/UserToken.scala",
    "content": "package net.wiringbits.repositories.models\n\nimport java.time.Instant\nimport java.util.UUID\n\ncase class UserToken(\n    id: UUID,\n    token: String,\n    tokenType: UserTokenType,\n    createdAt: Instant,\n    expiresAt: Instant,\n    userId: UUID\n)\n\nobject UserToken {\n\n  case class Create(\n      id: UUID,\n      token: String,\n      tokenType: UserTokenType,\n      createdAt: Instant,\n      expiresAt: Instant,\n      userId: UUID\n  )\n}\n"
  },
  {
    "path": "server/src/main/scala/net/wiringbits/repositories/models/UserTokenType.scala",
    "content": "package net.wiringbits.repositories.models\n\nimport enumeratum.EnumEntry.Uppercase\nimport enumeratum.{Enum, EnumEntry}\n\nsealed trait UserTokenType extends EnumEntry with Uppercase\n\nobject UserTokenType extends Enum[UserTokenType] {\n  case object EmailVerification extends UserTokenType\n  case object ResetPassword extends UserTokenType\n\n  val values = findValues\n}\n"
  },
  {
    "path": "server/src/main/scala/net/wiringbits/services/AdminService.scala",
    "content": "package net.wiringbits.services\n\nimport io.scalaland.chimney.dsl.transformInto\nimport net.wiringbits.api.models.admin.{AdminGetUserLogs, AdminGetUsers}\nimport net.wiringbits.repositories.{UserLogsRepository, UsersRepository}\n\nimport java.util.UUID\nimport javax.inject.Inject\nimport scala.concurrent.{ExecutionContext, Future}\n\nclass AdminService @Inject() (userLogsRepository: UserLogsRepository, usersRepository: UsersRepository)(implicit\n    ec: ExecutionContext\n) {\n\n  def userLogs(userId: UUID): Future[AdminGetUserLogs.Response] = {\n    for {\n      logs <- userLogsRepository.logs(userId)\n      items = logs.map(_.transformInto[AdminGetUserLogs.Response.UserLog])\n    } yield AdminGetUserLogs.Response(items)\n  }\n\n  def users(): Future[AdminGetUsers.Response] = {\n    for {\n      users <- usersRepository.all()\n      items = users.map(_.transformInto[AdminGetUsers.Response.User])\n    } yield AdminGetUsers.Response(items)\n  }\n}\n"
  },
  {
    "path": "server/src/main/scala/net/wiringbits/tasks/BackgroundJobsExecutorTask.scala",
    "content": "package net.wiringbits.tasks\n\nimport org.apache.pekko.actor.ActorSystem\nimport com.google.inject.Inject\nimport net.wiringbits.actions.internal.StreamPendingBackgroundJobsForeverAction\nimport net.wiringbits.apis.EmailApi\nimport net.wiringbits.apis.models.EmailRequest\nimport net.wiringbits.config.BackgroundJobsExecutorConfig\nimport net.wiringbits.models.jobs.{BackgroundJobPayload, BackgroundJobType}\nimport net.wiringbits.repositories.BackgroundJobsRepository\nimport net.wiringbits.repositories.models.BackgroundJobData\nimport net.wiringbits.util.{DelayGenerator, EmailMessage}\nimport org.slf4j.LoggerFactory\n\nimport java.time.Clock\nimport java.time.temporal.ChronoUnit\nimport scala.concurrent.duration.DurationInt\nimport scala.concurrent.{ExecutionContext, Future}\nimport scala.util.control.NonFatal\nimport scala.util.{Failure, Success}\n\nclass BackgroundJobsExecutorTask @Inject() (\n    config: BackgroundJobsExecutorConfig,\n    streamPendingBackgroundJobsForeverAction: StreamPendingBackgroundJobsForeverAction,\n    emailApi: EmailApi,\n    backgroundJobsRepository: BackgroundJobsRepository\n)(implicit\n    ec: ExecutionContext,\n    actorSystem: ActorSystem,\n    clock: Clock\n) {\n  private val logger = LoggerFactory.getLogger(this.getClass)\n\n  logger.info(\"Starting the background jobs executor task\")\n  actorSystem.scheduler.scheduleOnce(config.interval) {\n    run()\n  }\n\n  private def execute(job: BackgroundJobData): Future[Unit] = {\n    val executionResult = job.`type` match {\n      case BackgroundJobType.SendEmail =>\n        job.payload.asOpt[BackgroundJobPayload.SendEmail] match {\n          case Some(typedPayload) => sendEmail(typedPayload)\n          case None =>\n            Future.failed(\n              new RuntimeException(\n                s\"The given payload is not supported by the SendEmail task, please double check, job id = ${job.id}\"\n              )\n            )\n        }\n    }\n\n    executionResult\n      .flatMap { _ =>\n        backgroundJobsRepository.setStatusToSuccess(job.id)\n      }\n      .recoverWith { case NonFatal(ex) =>\n        val minutesUntilExecute = DelayGenerator.createDelay(job.errorCount)\n        val executeAt = clock.instant().plus(minutesUntilExecute, ChronoUnit.MINUTES)\n        logger.warn(s\"Job with id ${job.id} failed: ${ex.getMessage}\", ex)\n        backgroundJobsRepository.setStatusToFailed(job.id, executeAt, ex.getMessage)\n      }\n  }\n\n  // TODO: Move to another file\n  private def sendEmail(payload: BackgroundJobPayload.SendEmail): Future[Unit] = {\n    val emailRequest = EmailRequest(payload.email, EmailMessage(subject = payload.subject, body = payload.body))\n    emailApi.sendEmail(emailRequest)\n  }\n\n  def run(): Unit = {\n    // TODO: Allow configuring the throttling mechanism\n    // the reason to throttle and handle 1 background job concurrently is to avoid overloading the app\n    val result = streamPendingBackgroundJobsForeverAction()\n      .throttle(100, 1.minute)\n      .runWith(org.apache.pekko.stream.scaladsl.Sink.foreachAsync(1)(execute))\n\n    result.onComplete {\n      case Failure(ex) =>\n        logger.error(\n          s\"Failed to process pending background jobs, retrying after ${config.interval}: ${ex.getMessage}\",\n          ex\n        )\n        actorSystem.scheduler.scheduleOnce(config.interval) { run() }\n\n      case Success(_) => actorSystem.scheduler.scheduleOnce(config.interval) { run() }\n    }\n  }\n}\n"
  },
  {
    "path": "server/src/main/scala/net/wiringbits/util/DelayGenerator.scala",
    "content": "package net.wiringbits.util\n\nobject DelayGenerator {\n  def createDelay(\n      retry: Int,\n      factor: Int = 2\n  ): Long = {\n    Math\n      .pow(\n        factor.toDouble,\n        retry.toDouble\n      )\n      .longValue\n  }\n}\n"
  },
  {
    "path": "server/src/main/scala/net/wiringbits/util/EmailMessage.scala",
    "content": "package net.wiringbits.util\n\nimport net.wiringbits.common.models.Name\nimport org.apache.commons.text.StringEscapeUtils\n\ncase class EmailMessage(subject: String, body: String)\n\nobject EmailMessage {\n\n  implicit class EmailBodyStringExt(val str: String) extends AnyVal {\n    def htmlEscape: String = StringEscapeUtils.escapeHtml4(str)\n  }\n\n  def registration(name: Name, url: String, emailParameter: String): EmailMessage = {\n    val subject = \"Registration Confirmation\"\n    val body =\n      s\"\"\"Hi ${name.string.htmlEscape},\n         |Thanks for creating an account.\n         |To continue, please confirm your email address by clicking the button below.\n         |<a href=\"$url/verify-email/$emailParameter\">Confirm email address</a>\n         |\"\"\".stripMargin\n\n    EmailMessage(subject, body)\n  }\n\n  def confirm(name: Name): EmailMessage = {\n    val subject = \"Your email has been confirmed\"\n    val body = s\"Hi ${name.string.htmlEscape}, Thanks for confirming your email.\".stripMargin\n\n    EmailMessage(subject, body)\n  }\n\n  def forgotPassword(name: Name, url: String, emailParameter: String): EmailMessage = {\n    val subject = \"Password Reset\"\n    val body =\n      s\"\"\"<h2>Password Reset Instructions</h2>\n         |Hi ${name.string.htmlEscape},\n         |Here is the link to reset your password.\n         |To continue, please click the button below.\n         |<a href=\"$url/reset-password/$emailParameter\">Reset your password</a>\n         |If you did not perform this request, you can safely ignore this email.\n         |\"\"\".stripMargin\n\n    EmailMessage(subject, body)\n  }\n\n  def resetPassword(name: Name): EmailMessage = {\n    val subject = \"Your password has been reset\"\n    val body =\n      s\"\"\"Hi ${name.string.htmlEscape},\n         |<h2>Your password has been changed.</h2>\n         |If this was not you, click the 'Forgot password' link on the sign in page and follow the steps to reset your password.\n         |\"\"\".stripMargin\n\n    EmailMessage(subject, body)\n  }\n\n  def updatePassword(name: Name): EmailMessage = {\n    val subject = \"Your password has been updated\"\n    val body =\n      s\"\"\"Hi ${name.string.htmlEscape},\n         |<h2>Your password has been changed.</h2>\n         |If this was not you, click the 'Forgot password' link on the sign in page and follow the steps to reset your password.\n         |\"\"\".stripMargin\n\n    EmailMessage(subject, body)\n  }\n\n}\n"
  },
  {
    "path": "server/src/main/scala/net/wiringbits/util/EmailsHelper.scala",
    "content": "package net.wiringbits.util\n\nimport net.wiringbits.apis.EmailApi\nimport net.wiringbits.apis.models.EmailRequest\nimport net.wiringbits.config.{UserTokensConfig, WebAppConfig}\nimport net.wiringbits.repositories.UserTokensRepository\nimport net.wiringbits.repositories.models.{User, UserToken, UserTokenType}\n\nimport java.time.temporal.ChronoUnit\nimport java.time.{Clock, Instant}\nimport java.util.UUID\nimport javax.inject.Inject\nimport scala.concurrent.{ExecutionContext, Future}\n\nclass EmailsHelper @Inject() (\n    emailApi: EmailApi,\n    webAppConfig: WebAppConfig,\n    userTokensRepository: UserTokensRepository,\n    tokenGenerator: TokenGenerator,\n    userTokensConfig: UserTokensConfig,\n    clock: Clock\n)(implicit ec: ExecutionContext) {\n\n  def sendEmailVerificationToken(user: User): Future[Instant] = {\n    // we can't retrieve the plain text token, hence, we generate another one\n    val token = tokenGenerator.next()\n    val hmacToken = TokensHelper.doHMACSHA1(token.toString.getBytes(), userTokensConfig.hmacSecret)\n\n    val createToken = UserToken\n      .Create(\n        id = UUID.randomUUID(),\n        token = hmacToken,\n        tokenType = UserTokenType.EmailVerification,\n        createdAt = Instant.now(clock),\n        userId = user.id,\n        expiresAt = Instant.now(clock).plus(userTokensConfig.emailVerificationExp.toSeconds, ChronoUnit.SECONDS)\n      )\n\n    for {\n      _ <- userTokensRepository.create(createToken)\n      _ <- sendRegistrationEmailWithVerificationToken(user, token)\n    } yield createToken.expiresAt\n  }\n\n  // we don't save emails in the queue when user tokens are involved\n  def sendRegistrationEmailWithVerificationToken(user: User, token: UUID): Future[Unit] = {\n    val emailParameter = s\"${user.id}_$token\"\n    val emailMessage = EmailMessage.registration(\n      name = user.name,\n      url = webAppConfig.host,\n      emailParameter = emailParameter\n    )\n\n    val request = EmailRequest(user.email, emailMessage)\n    emailApi.sendEmail(request)\n  }\n\n  // we don't save emails in the queue when user tokens are involved\n  def sendPasswordRecoveryEmail(user: User): Future[Unit] = {\n    val token = tokenGenerator.next()\n    val emailParameter = s\"${user.id}_$token\"\n    val hmacToken = TokensHelper.doHMACSHA1(token.toString.getBytes, userTokensConfig.hmacSecret)\n    val createToken = UserToken\n      .Create(\n        id = UUID.randomUUID(),\n        token = hmacToken,\n        tokenType = UserTokenType.ResetPassword,\n        createdAt = Instant.now(clock),\n        userId = user.id,\n        expiresAt = Instant.now(clock).plus(userTokensConfig.resetPasswordExp.toHours, ChronoUnit.HOURS)\n      )\n    val message = EmailMessage.forgotPassword(user.name, webAppConfig.host, emailParameter)\n\n    for {\n      _ <- userTokensRepository.create(createToken)\n      _ <- emailApi.sendEmail(EmailRequest(user.email, message))\n    } yield ()\n  }\n}\n"
  },
  {
    "path": "server/src/main/scala/net/wiringbits/util/StringUtils.scala",
    "content": "package net.wiringbits.util\n\nobject StringUtils {\n\n  def mask(value: String, prefixSize: Int, suffixSize: Int): String = {\n    if (value.length <= prefixSize + suffixSize + 4) {\n      \"...\"\n    } else {\n      s\"${value.take(prefixSize)}...${value.takeRight(suffixSize)}\"\n    }\n  }\n\n  object Implicits {\n\n    implicit class StringUtilsExt(val string: String) extends AnyVal {\n      def mask(prefix: Int = 2, suffix: Int = 2): String = StringUtils.mask(string, prefix, suffix)\n    }\n  }\n}\n"
  },
  {
    "path": "server/src/main/scala/net/wiringbits/util/TokenGenerator.scala",
    "content": "package net.wiringbits.util\n\nimport java.util.UUID\nimport javax.inject.Inject\n\nclass TokenGenerator @Inject() () {\n  def next(): UUID = UUID.randomUUID()\n}\n"
  },
  {
    "path": "server/src/main/scala/net/wiringbits/util/TokensHelper.scala",
    "content": "package net.wiringbits.util\n\nimport jakarta.xml.bind.DatatypeConverter\n\nobject TokensHelper {\n\n  def doHMACSHA1(value: Array[Byte], secretKey: String): String = {\n    import javax.crypto.Mac\n    import javax.crypto.spec.SecretKeySpec\n    val signingKey = new SecretKeySpec(secretKey.getBytes, \"HmacSHA1\")\n    val mac = Mac.getInstance(\"HmacSHA1\")\n    mac.init(signingKey)\n    val rawHmac = mac.doFinal(value)\n    DatatypeConverter.printHexBinary(rawHmac)\n  }\n\n  def isSignatureValid(tokensSecret: String, digest: String, data: Array[Byte]): Boolean = {\n    val ourDigest = doHMACSHA1(data, tokensSecret)\n    ourDigest equalsIgnoreCase digest\n  }\n}\n"
  },
  {
    "path": "server/src/main/scala/net/wiringbits/validations/ValidateCaptcha.scala",
    "content": "package net.wiringbits.validations\n\nimport net.wiringbits.apis.ReCaptchaApi\nimport net.wiringbits.common.models.Captcha\n\nimport scala.concurrent.{ExecutionContext, Future}\n\nobject ValidateCaptcha {\n  def apply(captchaApi: ReCaptchaApi, captcha: Captcha)(implicit ec: ExecutionContext): Future[Unit] = {\n    captchaApi\n      .verify(captcha)\n      .map {\n        case true => ()\n        case false => throw new RuntimeException(s\"Invalid captcha, try again\")\n      }\n  }\n}\n"
  },
  {
    "path": "server/src/main/scala/net/wiringbits/validations/ValidateEmailIsAvailable.scala",
    "content": "package net.wiringbits.validations\n\nimport net.wiringbits.common.models.Email\nimport net.wiringbits.repositories.UsersRepository\n\nimport scala.concurrent.{ExecutionContext, Future}\n\nobject ValidateEmailIsAvailable {\n  def apply(repository: UsersRepository, email: Email)(implicit ec: ExecutionContext): Future[Unit] = {\n    for {\n      maybe <- repository.find(email)\n    } yield {\n      if (maybe.isDefined) throw new RuntimeException(s\"The email is not available\")\n      else ()\n    }\n  }\n}\n"
  },
  {
    "path": "server/src/main/scala/net/wiringbits/validations/ValidateEmailIsRegistered.scala",
    "content": "package net.wiringbits.validations\n\nimport net.wiringbits.common.models.Email\nimport net.wiringbits.repositories.UsersRepository\n\nimport scala.concurrent.{ExecutionContext, Future}\n\nobject ValidateEmailIsRegistered {\n  def apply(repository: UsersRepository, email: Email)(implicit ec: ExecutionContext): Future[Unit] = {\n    for {\n      maybe <- repository.find(email)\n    } yield {\n      if (maybe.isEmpty) throw new RuntimeException(s\"The email is not registered\")\n      else ()\n    }\n  }\n}\n"
  },
  {
    "path": "server/src/main/scala/net/wiringbits/validations/ValidatePasswordMatches.scala",
    "content": "package net.wiringbits.validations\n\nimport net.wiringbits.common.models.Password\nimport net.wiringbits.repositories.models.User\nimport org.mindrot.jbcrypt.BCrypt\n\nobject ValidatePasswordMatches {\n  def apply(maybe: Option[User], password: Password): User = {\n    maybe\n      .filter(user => BCrypt.checkpw(password.string, user.hashedPassword))\n      .getOrElse(throw new RuntimeException(\"The given email/password doesn't match\"))\n  }\n}\n"
  },
  {
    "path": "server/src/main/scala/net/wiringbits/validations/ValidateUserIsNotVerified.scala",
    "content": "package net.wiringbits.validations\n\nimport net.wiringbits.repositories.models.User\n\nobject ValidateUserIsNotVerified {\n  def apply(user: User): Unit = {\n    if (user.verifiedOn.isDefined)\n      throw new RuntimeException(s\"User email is already verified\")\n    else ()\n  }\n}\n"
  },
  {
    "path": "server/src/main/scala/net/wiringbits/validations/ValidateUserToken.scala",
    "content": "package net.wiringbits.validations\n\nimport net.wiringbits.repositories.models.UserToken\n\nimport java.time.Clock\n\nobject ValidateUserToken {\n  def apply(token: UserToken)(implicit clock: Clock): Unit = {\n    if (token.expiresAt.isBefore(clock.instant()))\n      throw new RuntimeException(\"Token is expired\")\n    else ()\n  }\n}\n"
  },
  {
    "path": "server/src/main/scala/net/wiringbits/validations/ValidateVerifiedUser.scala",
    "content": "package net.wiringbits.validations\n\nimport net.wiringbits.common.ErrorMessages\nimport net.wiringbits.repositories.models.User\n\nobject ValidateVerifiedUser {\n  def apply(user: User): Unit = {\n    if (user.verifiedOn.isDefined)\n      ()\n    else\n      throw new RuntimeException(ErrorMessages.emailNotVerified)\n  }\n}\n"
  },
  {
    "path": "server/src/test/scala/controllers/AdminControllerSpec.scala",
    "content": "package controllers\n\nimport com.dimafeng.testcontainers.PostgreSQLContainer\nimport controllers.common.PlayPostgresSpec\nimport net.wiringbits.apis.models.EmailRequest\nimport net.wiringbits.apis.{EmailApi, ReCaptchaApi}\nimport net.wiringbits.common.models.*\nimport net.wiringbits.util.TokenGenerator\nimport org.mockito.ArgumentMatchers.any\nimport org.mockito.Mockito.*\nimport org.scalatestplus.mockito.MockitoSugar\nimport play.api.inject\nimport play.api.inject.guice.GuiceApplicationBuilder\nimport utils.LoginUtils\n\nimport java.time.{Clock, Instant}\nimport java.util.UUID\nimport scala.concurrent.Future\n\nclass AdminControllerSpec extends PlayPostgresSpec with LoginUtils with MockitoSugar {\n  private val tokenGenerator = mock[TokenGenerator]\n\n  private val clock = mock[Clock]\n  when(clock.instant).thenReturn(Instant.now())\n\n  private val emailApi = mock[EmailApi]\n  when(emailApi.sendEmail(any[EmailRequest]())).thenReturn(Future.unit)\n\n  private val captchaApi = mock[ReCaptchaApi]\n  when(captchaApi.verify(any[Captcha]())).thenReturn(Future.successful(true))\n\n  override def guiceApplicationBuilder(container: PostgreSQLContainer): GuiceApplicationBuilder =\n    super\n      .guiceApplicationBuilder(container)\n      .overrides(\n        inject.bind[TokenGenerator].to(tokenGenerator),\n        inject.bind[EmailApi].to(emailApi),\n        inject.bind[ReCaptchaApi].to(captchaApi),\n        inject.bind[Clock].to(clock)\n      )\n\n  \"GET /admin/users\" should {\n    \"get every user\" in withApiClient { implicit client =>\n      val expected = 3\n      (1 to expected).foreach { _ =>\n        createVerifyLoginUser(\n          tokenGenerator\n        ).futureValue\n      }\n\n      val response = client.adminGetUsers.futureValue\n      response.data.length must be(expected)\n    }\n\n    \"return no results\" in withApiClient { client =>\n      val response = client.adminGetUsers.futureValue\n      response.data.isEmpty must be(true)\n    }\n  }\n\n  \"GET /admin/users/:userId/logs\" should {\n    \"get user logs\" in withApiClient { implicit client =>\n      val user = createVerifyLoginUser(tokenGenerator).futureValue\n      val response = client.adminGetUserLogs(user.id).futureValue\n      response.data.isEmpty must be(false)\n    }\n\n    \"return no results\" in withApiClient { client =>\n      val response = client.adminGetUserLogs(UUID.randomUUID()).futureValue\n      response.data.isEmpty must be(true)\n    }\n  }\n}\n"
  },
  {
    "path": "server/src/test/scala/controllers/AuthControllerSpec.scala",
    "content": "package controllers\n\nimport com.dimafeng.testcontainers.PostgreSQLContainer\nimport controllers.common.PlayPostgresSpec\nimport net.wiringbits.api.models.auth.Login\nimport net.wiringbits.api.models.users.VerifyEmail\nimport net.wiringbits.apis.models.EmailRequest\nimport net.wiringbits.apis.{EmailApi, ReCaptchaApi}\nimport net.wiringbits.common.models.*\nimport net.wiringbits.config.UserTokensConfig\nimport net.wiringbits.repositories.UserTokensRepository\nimport net.wiringbits.util.TokenGenerator\nimport org.mockito.ArgumentMatchers.any\nimport org.mockito.Mockito.*\nimport org.scalatestplus.mockito.MockitoSugar\nimport play.api.inject\nimport play.api.inject.guice.GuiceApplicationBuilder\nimport utils.LoginUtils\n\nimport java.time.temporal.ChronoUnit\nimport java.time.{Clock, Instant}\nimport java.util.UUID\nimport scala.concurrent.Future\n\nclass AuthControllerSpec extends PlayPostgresSpec with LoginUtils with MockitoSugar {\n\n  def userTokensRepository: UserTokensRepository = app.injector.instanceOf(classOf[UserTokensRepository])\n\n  private val clock = mock[Clock]\n  when(clock.instant).thenReturn(Instant.now())\n\n  private val tokenGenerator = mock[TokenGenerator]\n\n  private val emailApi = mock[EmailApi]\n  when(emailApi.sendEmail(any[EmailRequest]())).thenReturn(Future.unit)\n\n  private val captchaApi = mock[ReCaptchaApi]\n  when(captchaApi.verify(any[Captcha]())).thenReturn(Future.successful(true))\n\n  override def guiceApplicationBuilder(container: PostgreSQLContainer): GuiceApplicationBuilder =\n    super\n      .guiceApplicationBuilder(container)\n      .overrides(\n        inject.bind[EmailApi].to(emailApi),\n        inject.bind[ReCaptchaApi].to(captchaApi),\n        inject.bind[Clock].to(clock),\n        inject.bind[TokenGenerator].to(tokenGenerator)\n      )\n\n  def userTokensConfig: UserTokensConfig = app.injector.instanceOf(classOf[UserTokensConfig])\n\n  \"POST /auth/login\" should {\n    \"return the response from a correct user\" in withApiClient { implicit client =>\n      val name = Name.trusted(\"wiringbits\")\n      val email = Email.trusted(\"test1@email.com\")\n      val loginResponse =\n        createVerifyLoginUser(tokenGenerator, nameMaybe = Some(name), emailMaybe = Some(email)).futureValue\n\n      loginResponse.name must be(name)\n      loginResponse.email must be(email)\n    }\n\n    \"fail when the user tries to login without an email verification\" in withApiClient { implicit client =>\n      val password = Password.trusted(\"test123...\")\n      val user = createUser(passwordMaybe = Some(password)).futureValue\n\n      val loginRequest = Login.Request(\n        email = user.email,\n        password = password,\n        captcha = Captcha.trusted(\"test\")\n      )\n\n      val error = client\n        .login(loginRequest)\n        .expectError\n\n      error must be(\"The email is not verified, check your spam folder if you don't see the email.\")\n    }\n\n    \"fail when the user tries to verify with a wrong token\" in withApiClient { implicit client =>\n      val user = createUser().futureValue\n\n      val error = client\n        .verifyEmail(VerifyEmail.Request(UserToken(user.id, UUID.randomUUID())))\n        .expectError\n\n      error must be(s\"Token for user ${user.id} wasn't found\")\n    }\n\n    \"fail when the user tries to verify with an expired token\" in withApiClient { implicit client =>\n      val verificationToken = UUID.randomUUID()\n      when(tokenGenerator.next()).thenReturn(verificationToken)\n\n      val user = createUser().futureValue\n\n      when(clock.instant).thenReturn(Instant.now().plus(2, ChronoUnit.DAYS))\n\n      val error = client\n        .verifyEmail(VerifyEmail.Request(UserToken(user.id, verificationToken)))\n        .expectError\n\n      error must be(\"Token is expired\")\n    }\n\n    \"login after successful email confirmation\" in withApiClient { implicit client =>\n      val email = Email.trusted(\"test1@email.com\")\n      val response = createVerifyLoginUser(tokenGenerator, emailMaybe = Some(email)).futureValue\n\n      response.email must be(email)\n    }\n\n    \"fail when password is incorrect\" in withApiClient { implicit client =>\n      val verificationToken = UUID.randomUUID()\n      when(tokenGenerator.next()).thenReturn(verificationToken)\n\n      val user = createUser().futureValue\n\n      client.verifyEmail(VerifyEmail.Request(UserToken(user.id, verificationToken))).futureValue\n\n      val loginRequest = Login.Request(\n        email = user.email,\n        password = Password.trusted(\"Incorrect password\"),\n        captcha = Captcha.trusted(\"test\")\n      )\n\n      val error = client\n        .login(loginRequest)\n        .expectError\n\n      error must be(\"The given email/password doesn't match\")\n    }\n\n    \"fail when the captcha isn't valid\" in withApiClient { implicit client =>\n      val password = Password.trusted(\"test123...\")\n      val user = createUser(passwordMaybe = Some(password)).futureValue\n\n      val loginRequest = Login.Request(\n        email = user.email,\n        password = password,\n        captcha = Captcha.trusted(\"test\")\n      )\n\n      when(captchaApi.verify(any[Captcha]())).thenReturn(Future.successful(false))\n\n      val error = client\n        .login(loginRequest)\n        .expectError\n\n      error must be(\"Invalid captcha, try again\")\n\n      when(captchaApi.verify(any[Captcha]())).thenReturn(Future.successful(true))\n    }\n\n    \"fail when user isn't email confirmed\" in withApiClient { implicit client =>\n      val password = Password.trusted(\"test123...\")\n      val user = createUser(passwordMaybe = Some(password)).futureValue\n\n      val loginRequest = Login.Request(\n        email = user.email,\n        password = password,\n        captcha = Captcha.trusted(\"test\")\n      )\n\n      val error = client\n        .login(loginRequest)\n        .expectError\n\n      error must be(\"The email is not verified, check your spam folder if you don't see the email.\")\n    }\n  }\n\n  \"GET /auth/me\" should {\n    \"return current logged user\" in withApiClient { implicit client =>\n      val name = Name.trusted(\"wiringbits\")\n      val email = Email.trusted(\"test1@email.com\")\n      createVerifyLoginUser(tokenGenerator, nameMaybe = Some(name), emailMaybe = Some(email)).futureValue\n      val currentUser = client.currentUser.futureValue\n\n      currentUser.name must be(name)\n      currentUser.email must be(email)\n    }\n\n    \"fail if user isn't logged in\" in withApiClient { client =>\n      val error = client.currentUser.expectError\n      error must be(\"Unauthorized: Invalid or missing authentication\")\n    }\n  }\n}\n"
  },
  {
    "path": "server/src/test/scala/controllers/EnvironmentConfigControllerSpec.scala",
    "content": "package controllers\n\nimport controllers.common.PlayPostgresSpec\nimport net.wiringbits.config.ReCaptchaConfig\nimport org.scalatest.BeforeAndAfterAll\n\nclass EnvironmentConfigControllerSpec extends PlayPostgresSpec {\n  def reCaptchaConfig: ReCaptchaConfig = app.injector.instanceOf(classOf[ReCaptchaConfig])\n\n  \"GET /environment-config\" should {\n    \"return the frontend configuration\" in withApiClient { client =>\n      val response = client.getEnvironmentConfig.futureValue\n      response.recaptchaSiteKey must be(reCaptchaConfig.siteKey.string)\n    }\n  }\n}\n"
  },
  {
    "path": "server/src/test/scala/controllers/UsersControllerSpec.scala",
    "content": "package controllers\n\nimport com.dimafeng.testcontainers.PostgreSQLContainer\nimport controllers.common.PlayPostgresSpec\nimport net.wiringbits.api.models.auth.Login\nimport net.wiringbits.api.models.users.{ForgotPassword, ResetPassword, SendEmailVerificationToken, VerifyEmail}\nimport net.wiringbits.apis.models.EmailRequest\nimport net.wiringbits.apis.{EmailApi, ReCaptchaApi}\nimport net.wiringbits.common.models.*\nimport net.wiringbits.config.UserTokensConfig\nimport net.wiringbits.repositories.UserTokensRepository\nimport net.wiringbits.repositories.models.UserTokenType\nimport net.wiringbits.util.{TokenGenerator, TokensHelper}\nimport org.mockito.ArgumentMatchers.any\nimport org.mockito.Mockito.*\nimport org.scalatestplus.mockito.MockitoSugar\nimport play.api.inject\nimport play.api.inject.guice.GuiceApplicationBuilder\nimport utils.LoginUtils\n\nimport java.time.{Clock, Instant}\nimport java.util.UUID\nimport scala.concurrent.Future\n\nclass UsersControllerSpec extends PlayPostgresSpec with LoginUtils with MockitoSugar {\n\n  def userTokensRepository: UserTokensRepository = app.injector.instanceOf(classOf[UserTokensRepository])\n\n  private val clock = mock[Clock]\n  when(clock.instant).thenReturn(Instant.now())\n\n  private val tokenGenerator = mock[TokenGenerator]\n\n  private val emailApi = mock[EmailApi]\n  when(emailApi.sendEmail(any[EmailRequest]())).thenReturn(Future.unit)\n\n  private val captchaApi = mock[ReCaptchaApi]\n  when(captchaApi.verify(any[Captcha]())).thenReturn(Future.successful(true))\n\n  def userTokensConfig: UserTokensConfig = app.injector.instanceOf(classOf[UserTokensConfig])\n\n  override def guiceApplicationBuilder(container: PostgreSQLContainer): GuiceApplicationBuilder =\n    super\n      .guiceApplicationBuilder(container)\n      .overrides(\n        inject.bind[EmailApi].to(emailApi),\n        inject.bind[ReCaptchaApi].to(captchaApi),\n        inject.bind[Clock].to(clock),\n        inject.bind[TokenGenerator].to(tokenGenerator)\n      )\n\n  private def createHMACToken(token: UUID): String = {\n    TokensHelper.doHMACSHA1(token.toString.getBytes, app.injector.instanceOf[UserTokensConfig].hmacSecret)\n  }\n\n  \"POST /users\" should {\n    \"return the email verification token after creating a user\" in withApiClient { implicit client =>\n      val name = Name.trusted(\"wiringbits\")\n      val email = Email.trusted(\"test1@email.com\")\n\n      val verificationToken = UUID.randomUUID()\n      when(tokenGenerator.next()).thenReturn(verificationToken)\n\n      val response = createUser(nameMaybe = Some(name), emailMaybe = Some(email)).futureValue\n      val token = userTokensRepository\n        .find(response.id)\n        .futureValue\n        .find(_.tokenType == UserTokenType.EmailVerification)\n        .value\n\n      response.name must be(name)\n      response.email must be(email)\n      token.token must be(createHMACToken(verificationToken))\n    }\n\n    \"fail when the email is already taken\" in withApiClient { implicit client =>\n      val email = Email.trusted(\"test@wiringbits.net\")\n      createUser(emailMaybe = Some(email)).futureValue\n\n      val error = createUser(emailMaybe = Some(email)).expectError\n      error must be(\"The email is not available\")\n    }\n\n    \"fail when the captcha isn't valid\" in withApiClient { implicit client =>\n      when(captchaApi.verify(any[Captcha]())).thenReturn(Future.successful(false))\n      val error = createUser().expectError\n\n      error must be(\"Invalid captcha, try again\")\n\n      when(captchaApi.verify(any[Captcha]())).thenReturn(Future.successful(true))\n    }\n  }\n\n  \"POST /users/verify-email\" should {\n    \"success on verifying user's email\" in withApiClient { implicit client =>\n      val name = Name.trusted(\"wiringbits\")\n      val email = Email.trusted(\"test1@email.com\")\n      val response = createVerifyLoginUser(tokenGenerator, nameMaybe = Some(name), emailMaybe = Some(email)).futureValue\n\n      response.name must be(name)\n      response.email must be(email)\n    }\n\n    \"delete the verification token after successful email confirmation\" in withApiClient { implicit client =>\n      val user = createVerifyLoginUser(tokenGenerator).futureValue\n\n      userTokensRepository.find(user.id).futureValue must be(empty)\n    }\n\n    \"fail when trying to verify an already verified user's email\" in withApiClient { implicit client =>\n      val user = createVerifyLoginUser(tokenGenerator).futureValue\n\n      val token = UUID.randomUUID()\n\n      val error = client\n        .verifyEmail(VerifyEmail.Request(UserToken(user.id, token)))\n        .expectError\n\n      error must be(s\"User email is already verified\")\n    }\n  }\n\n  \"POST /forgot-password\" should {\n    \"create the reset password token after the user's request to reset their password\" in withApiClient {\n      implicit client =>\n        val email = Email.trusted(\"test1@email.com\")\n        createVerifyLoginUser(tokenGenerator, emailMaybe = Some(email)).futureValue\n\n        val forgotPasswordRequest = ForgotPassword.Request(email, Captcha.trusted(\"test\"))\n        val response = client.forgotPassword(forgotPasswordRequest).futureValue\n\n        response must be(ForgotPassword.Response())\n    }\n\n    \"ignore the request when the user tries to reset a password for nonexistent email\" in withApiClient { client =>\n      val email = Email.trusted(\"test@email.com\")\n      val forgotPasswordRequest = ForgotPassword.Request(email, Captcha.trusted(\"test\"))\n\n      val response = client.forgotPassword(forgotPasswordRequest).futureValue\n\n      response must be(ForgotPassword.Response())\n    }\n\n    \"fail when the user tries to reset a password without their email verification step\" in withApiClient {\n      implicit client =>\n        val email = Email.trusted(\"test1@email.com\")\n        createUser(emailMaybe = Some(email)).futureValue\n\n        val forgotPasswordRequest = ForgotPassword.Request(email, Captcha.trusted(\"test\"))\n\n        val error = client\n          .forgotPassword(forgotPasswordRequest)\n          .expectError\n\n        error must be(s\"The email is not verified, check your spam folder if you don't see the email.\")\n    }\n\n    \"fail when the captcha isn't valid\" in withApiClient { implicit client =>\n      val email = Email.trusted(\"test@email.com\")\n\n      createVerifyLoginUser(tokenGenerator, emailMaybe = Some(email)).futureValue\n\n      val forgotPasswordRequest = ForgotPassword.Request(email, Captcha.trusted(\"test\"))\n      when(captchaApi.verify(any[Captcha]())).thenReturn(Future.successful(false))\n\n      val error = client\n        .forgotPassword(forgotPasswordRequest)\n        .expectError\n\n      error must be(\"Invalid captcha, try again\")\n\n      when(captchaApi.verify(any[Captcha]())).thenReturn(Future.successful(true))\n    }\n  }\n\n  \"POST /reset-password\" should {\n    \"reset a password for a given user\" in withApiClient { implicit client =>\n      val name = Name.trusted(\"wiringbits\")\n      val email = Email.trusted(\"test@email.com\")\n      val user = createVerifyLoginUser(tokenGenerator, nameMaybe = Some(name), emailMaybe = Some(email)).futureValue\n\n      val verificationToken = UUID.randomUUID()\n      when(tokenGenerator.next()).thenReturn(verificationToken)\n\n      val forgotPasswordRequest = ForgotPassword.Request(email, Captcha.trusted(\"test\"))\n      client.forgotPassword(forgotPasswordRequest).futureValue\n\n      val resetPasswordRequest =\n        ResetPassword.Request(UserToken(user.id, verificationToken), Password.trusted(\"test456...\"))\n      client.resetPassword(resetPasswordRequest).futureValue\n\n      val loginRequest = Login.Request(\n        email = email,\n        password = Password.trusted(\"test456...\"),\n        captcha = Captcha.trusted(\"test\")\n      )\n      val loginResponse = client.login(loginRequest).futureValue\n\n      loginResponse.name must be(name)\n      loginResponse.email must be(email)\n    }\n\n    \"return a email when a user tries to reset a password\" in withApiClient { implicit client =>\n      val name = Name.trusted(\"wiringbits\")\n      val email = Email.trusted(\"test1@email.com\")\n\n      val userId =\n        createVerifyLoginUser(tokenGenerator, nameMaybe = Some(name), emailMaybe = Some(email)).futureValue.id\n\n      val verificationToken = tokenGenerator.next()\n\n      val forgotPasswordRequest = ForgotPassword.Request(email, Captcha.trusted(\"test\"))\n      client.forgotPassword(forgotPasswordRequest).futureValue\n\n      val resetPasswordRequest =\n        ResetPassword.Request(UserToken(userId, verificationToken), Password.trusted(\"test456...\"))\n\n      val response = client\n        .resetPassword(resetPasswordRequest)\n        .futureValue\n\n      response.email must be(email)\n    }\n\n    \"fail when the user tries to login with their old password after the password resetting\" in withApiClient {\n      implicit client =>\n        val email = Email.trusted(\"test1@email.com\")\n        val user = createVerifyLoginUser(tokenGenerator, emailMaybe = Some(email)).futureValue\n\n        val verificationToken = UUID.randomUUID()\n        when(tokenGenerator.next()).thenReturn(verificationToken)\n\n        val forgotPasswordRequest = ForgotPassword.Request(email, Captcha.trusted(\"test\"))\n        client.forgotPassword(forgotPasswordRequest).futureValue\n\n        val resetPasswordRequest =\n          ResetPassword.Request(UserToken(user.id, verificationToken), Password.trusted(\"test456...\"))\n        client.resetPassword(resetPasswordRequest).futureValue\n\n        val loginRequest = Login.Request(\n          email = email,\n          password = Password.trusted(\"test123...\"),\n          captcha = Captcha.trusted(\"test\")\n        )\n\n        val error = client\n          .login(loginRequest)\n          .expectError\n\n        error must be(\"The given email/password doesn't match\")\n    }\n  }\n\n  \"POST /users/email-verification-token\" should {\n    \"success on send verifying token user's email\" in withApiClient { implicit client =>\n      val name = Name.trusted(\"wiringbits\")\n      val email = Email.trusted(\"test@email.com\")\n      val request = SendEmailVerificationToken.Request(\n        email = email,\n        captcha = Captcha.trusted(\"test\")\n      )\n\n      val verificationToken = UUID.randomUUID()\n      when(tokenGenerator.next()).thenReturn(verificationToken)\n\n      val userCreated = createUser(nameMaybe = Some(name), emailMaybe = Some(email)).futureValue\n\n      val response = client.sendEmailVerificationToken(request).futureValue\n\n      val token = userTokensRepository\n        .find(userCreated.id)\n        .futureValue\n        .find(_.tokenType == UserTokenType.EmailVerification)\n        .value\n\n      response.expiresAt must be(token.expiresAt)\n    }\n\n    \"success on verifying email and login\" in withApiClient { implicit client =>\n      val name = Name.trusted(\"wiringbits\")\n      val email = Email.trusted(\"test@email.com\")\n      val response = createVerifyLoginUser(tokenGenerator, nameMaybe = Some(name), emailMaybe = Some(email)).futureValue\n\n      response.name must be(name)\n      response.email must be(email)\n    }\n\n    \"fail when user's email is not registered\" in withApiClient { client =>\n      val email = Email.trusted(\"test@email.com\")\n      val request = SendEmailVerificationToken.Request(\n        email = email,\n        captcha = Captcha.trusted(\"test\")\n      )\n      val error = client.sendEmailVerificationToken(request).expectError\n\n      error must be(s\"The email is not registered\")\n    }\n\n    \"fail when the captcha isn't valid\" in withApiClient { client =>\n      val email = Email.trusted(\"test@email.com\")\n      val request = SendEmailVerificationToken.Request(\n        email = email,\n        captcha = Captcha.trusted(\"test\")\n      )\n\n      when(captchaApi.verify(any[Captcha]())).thenReturn(Future.successful(false))\n\n      val error = client\n        .sendEmailVerificationToken(request)\n        .expectError\n\n      error must be(\"Invalid captcha, try again\")\n\n      when(captchaApi.verify(any[Captcha]())).thenReturn(Future.successful(true))\n    }\n\n    \"fail if the user is already verified\" in withApiClient { implicit client =>\n      val email = Email.trusted(\"test@email.com\")\n      val request = SendEmailVerificationToken.Request(\n        email = email,\n        captcha = Captcha.trusted(\"test\")\n      )\n\n      val verificationToken = UUID.randomUUID()\n      when(tokenGenerator.next()).thenReturn(verificationToken)\n\n      createVerifyLoginUser(tokenGenerator, emailMaybe = Some(email)).futureValue\n\n      val error = client\n        .sendEmailVerificationToken(request)\n        .expectError\n\n      error must be(s\"User email is already verified\")\n    }\n  }\n}\n"
  },
  {
    "path": "server/src/test/scala/controllers/common/PlayAPISpec.scala",
    "content": "package controllers.common\n\nimport org.scalatest.concurrent.ScalaFutures\nimport org.scalatestplus.play.PlaySpec\nimport org.slf4j.LoggerFactory\nimport play.api.inject.guice.GuiceApplicationBuilder\nimport play.api.mvc.Result\nimport play.api.test.FakeRequest\nimport play.api.test.Helpers.*\nimport play.api.{Application, Mode}\n\nimport java.net.URLEncoder\nimport scala.concurrent.Future\n\n/** A PlayAPISpec allow us to write tests for the API calls.\n  */\ntrait PlayAPISpec extends PlaySpec with ScalaFutures {\n\n  protected def defaultGuiceApplicationBuilder: GuiceApplicationBuilder =\n    GuiceApplicationBuilder()\n      .in(Mode.Test)\n\n  private val JsonHeader = CONTENT_TYPE -> \"application/json\"\n  private val EmptyJson = \"{}\"\n\n  protected val logger = LoggerFactory.getLogger(this.getClass)\n\n  def log[T](request: FakeRequest[T], response: Future[Result]): Unit = {\n    logger.info(\n      s\"REQUEST > $request, headers = ${request.headers}; RESPONSE < status = ${status(response)}, body = ${contentAsString(response)}\"\n    )\n  }\n\n  /** Syntactic sugar for calling APIs * */\n  def GET(url: String, extraHeaders: (String, String)*)(implicit application: Application): Future[Result] = {\n    val headers = JsonHeader :: extraHeaders.toList\n    val request = FakeRequest(\"GET\", url)\n      .withHeaders(headers: _*)\n\n    val response = route(application, request).value\n    log(request, response)\n    response\n  }\n\n  def POST(url: String, extraHeaders: (String, String)*)(implicit application: Application): Future[Result] = {\n    POST(url, EmptyJson, extraHeaders: _*)\n  }\n\n  def POST(url: String, jsonBody: String, extraHeaders: (String, String)*)(implicit\n      application: Application\n  ): Future[Result] = {\n    val headers = JsonHeader :: extraHeaders.toList\n    val request = FakeRequest(\"POST\", url)\n      .withHeaders(headers: _*)\n      .withBody(jsonBody)\n\n    val response = route(application, request).value\n    log(request, response)\n    response\n  }\n\n  def PUT(url: String, extraHeaders: (String, String)*)(implicit application: Application): Future[Result] = {\n    PUT(url, EmptyJson, extraHeaders: _*)\n  }\n\n  def PUT(url: String, jsonBody: String, extraHeaders: (String, String)*)(implicit\n      application: Application\n  ): Future[Result] = {\n    val headers = JsonHeader :: extraHeaders.toList\n    val request = FakeRequest(\"PUT\", url)\n      .withHeaders(headers: _*)\n      .withBody(jsonBody)\n\n    val response = route(application, request).value\n    log(request, response)\n    response\n  }\n\n  def DELETE(url: String, extraHeaders: (String, String)*)(implicit application: Application): Future[Result] = {\n    val headers = JsonHeader :: extraHeaders.toList\n    val request = FakeRequest(\"DELETE\", url)\n      .withHeaders(headers: _*)\n\n    val response = route(application, request).value\n    log(request, response)\n\n    response\n  }\n}\n\nobject PlayAPISpec {\n\n  object Implicits {\n\n    implicit class HttpExt(val params: List[(String, String)]) extends AnyVal {\n\n      def toQueryString: String = {\n        params\n          .map { case (key, value) =>\n            val encodedKey = URLEncoder.encode(key, \"UTF-8\")\n            val encodedValue = URLEncoder.encode(value, \"UTF-8\")\n            List(encodedKey, encodedValue).mkString(\"=\")\n          }\n          .mkString(\"&\")\n      }\n    }\n\n    implicit class StringUrlExt(val url: String) extends AnyVal {\n\n      def withQueryParams(params: (String, String)*): String = {\n        List(url, params.toList.toQueryString).mkString(\"?\")\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "server/src/test/scala/controllers/common/PlayPostgresSpec.scala",
    "content": "package controllers.common\n\nimport com.dimafeng.testcontainers.PostgreSQLContainer\nimport com.dimafeng.testcontainers.scalatest.TestContainerForEach\nimport net.wiringbits.api.ApiClient\nimport org.scalatest.TestData\nimport org.scalatest.time.SpanSugar.convertIntToGrainOfTime\nimport org.scalatestplus.play.guice.GuiceOneServerPerTest\nimport org.testcontainers.utility.DockerImageName\nimport play.api.inject.guice.GuiceApplicationBuilder\nimport play.api.{Application, Configuration, Environment, Mode}\nimport sttp.client3.HttpClientFutureBackend\n\nimport java.sql.DriverManager\nimport scala.concurrent.{ExecutionContext, Future}\nimport scala.util.control.NonFatal\n\ntrait PlayPostgresSpec extends PlayAPISpec with TestContainerForEach with GuiceOneServerPerTest {\n  implicit val executionContext: ExecutionContext = ExecutionContext.Implicits.global\n  override implicit val patienceConfig: PatienceConfig = PatienceConfig(30.seconds, 1.second)\n\n  private val postgresImage = DockerImageName.parse(\"postgres:13\")\n  override val containerDef: PostgreSQLContainer.Def = PostgreSQLContainer.Def(dockerImageName = postgresImage)\n\n  /** Loads configuration disabling evolutions on default database.\n    *\n    * This allows to not write a custom application.conf for testing and ensure play evolutions are disabled.\n    */\n  private def loadConfigWithoutEvolutions(env: Environment, container: PostgreSQLContainer): Configuration = {\n    val map = Map(\n      \"db.default.username\" -> container.username,\n      \"db.default.password\" -> container.password,\n      \"db.default.url\" -> container.jdbcUrl\n    )\n\n    Configuration.from(map).withFallback(Configuration.load(env))\n  }\n\n  def guiceApplicationBuilder(container: PostgreSQLContainer): GuiceApplicationBuilder =\n    GuiceApplicationBuilder(loadConfiguration = env => loadConfigWithoutEvolutions(env, container))\n      .in(Mode.Test)\n\n  override def newAppForTest(testData: TestData): Application = {\n    withContainers { postgres =>\n      val conn = DriverManager.getConnection(\n        postgres.container.getJdbcUrl,\n        postgres.container.getUsername,\n        postgres.container.getPassword\n      )\n      conn.createStatement().execute(\"CREATE EXTENSION CITEXT;\")\n      conn.close()\n\n      guiceApplicationBuilder(postgres).build()\n    }\n  }\n\n  def withApiClient[A](runTest: ApiClient => A): A = {\n    implicit val sttpBackend: sttp.client3.SttpBackend[concurrent.Future, _] = HttpClientFutureBackend()\n\n    val config = ApiClient.Config(s\"http://localhost:$port\")\n    val client = new ApiClient(config)\n    runTest(client)\n  }\n\n  implicit class RichFutureExt[T](val future: Future[T]) {\n    def expectError: String = {\n      future\n        .map(_ => \"Success when failure expected\")\n        .recover { case NonFatal(ex) =>\n          ex.getMessage\n        }\n        .futureValue\n    }\n  }\n}\n"
  },
  {
    "path": "server/src/test/scala/net/wiringbits/apis/ReCaptchaApiSpec.scala",
    "content": "package net.wiringbits.apis\n\nimport net.wiringbits.common.models.Captcha\nimport net.wiringbits.config.ReCaptchaConfig\nimport net.wiringbits.models.{ReCaptchaSecret, ReCaptchaSiteKey}\nimport org.mockito.ArgumentMatchers\nimport org.mockito.Mockito.*\nimport org.scalatest.concurrent.ScalaFutures.*\nimport org.scalatest.matchers.must.Matchers.*\nimport org.scalatest.wordspec.AnyWordSpec\nimport org.scalatestplus.mockito.MockitoSugar\nimport play.api.libs.json.{JsValue, Json}\nimport play.api.libs.ws.DefaultBodyWritables.*\nimport play.api.libs.ws.{BodyWritable, WSClient, WSRequest, WSResponse}\n\nimport scala.concurrent.ExecutionContext.Implicits.global\nimport scala.concurrent.Future\n\nclass ReCaptchaApiSpec extends AnyWordSpec with MockitoSugar {\n  private val ws = mock[WSClient]\n  private val request = mock[WSRequest]\n  private val response = mock[WSResponse]\n  private val config = ReCaptchaConfig(ReCaptchaSecret(\"test\"), ReCaptchaSiteKey(\"test\"))\n  private val api = new ReCaptchaApi(config, ws)\n\n  \"verify\" should {\n    \"detect successful responses\" in {\n      mockRequest(request, response)(Json.obj(\"success\" -> true))\n      val result = api.verify(Captcha.trusted(\"example\"))\n      result.futureValue must be(true)\n    }\n\n    \"detect unsuccessful responses\" in {\n      mockRequest(request, response)(Json.obj(\"success\" -> false))\n      val result = api.verify(Captcha.trusted(\"example\"))\n      result.futureValue must be(false)\n    }\n\n    \"fail when getting an unknown response\" in {\n      mockRequest(request, response)(Json.obj(\"other\" -> false))\n      val result = api.verify(Captcha.trusted(\"example\"))\n      intercept[Throwable](result.futureValue)\n    }\n  }\n\n  private def mockRequest(request: WSRequest, response: WSResponse)(body: JsValue): Unit = {\n    when(ws.url(ArgumentMatchers.anyString)).thenReturn(request)\n    when(request.addQueryStringParameters(ArgumentMatchers.any[(String, String)])).thenReturn(request)\n    when(response.json).thenReturn(body)\n    val _ =\n      when(request.post[String](ArgumentMatchers.anyString())(ArgumentMatchers.eq(implicitly[BodyWritable[String]])))\n        .thenReturn(Future.successful(response))\n  }\n}\n"
  },
  {
    "path": "server/src/test/scala/net/wiringbits/core/PostgresSpec.scala",
    "content": "package net.wiringbits.core\n\nimport com.dimafeng.testcontainers.PostgreSQLContainer\nimport com.dimafeng.testcontainers.scalatest.TestContainerForEach\nimport org.scalatest.Suite\nimport org.testcontainers.utility.DockerImageName\nimport play.api.db.evolutions.Evolutions\nimport play.api.db.{Database, Databases}\n\nimport java.sql.DriverManager\n\ntrait PostgresSpec extends TestContainerForEach {\n  self: Suite =>\n  private val postgresImage = DockerImageName.parse(\"postgres:13\")\n  override val containerDef: PostgreSQLContainer.Def = PostgreSQLContainer.Def(dockerImageName = postgresImage)\n\n  def initDatabase(postgres: Containers): Unit = {\n    val conn = DriverManager.getConnection(\n      postgres.container.getJdbcUrl,\n      postgres.container.getUsername,\n      postgres.container.getPassword\n    )\n    conn.createStatement().execute(\"CREATE EXTENSION CITEXT;\")\n    conn.close()\n  }\n\n  def withDatabase[T](runTest: Database => T): T = withContainers { postgres =>\n    initDatabase(postgres)\n\n    val database = Databases(\n      driver = \"org.postgresql.Driver\",\n      url = postgres.jdbcUrl,\n      name = \"default\",\n      config = Map(\n        \"username\" -> postgres.container.getUsername,\n        \"password\" -> postgres.container.getPassword\n      )\n    )\n\n    Evolutions.applyEvolutions(database)\n\n    runTest(database)\n  }\n}\n"
  },
  {
    "path": "server/src/test/scala/net/wiringbits/core/RepositoryComponents.scala",
    "content": "package net.wiringbits.core\n\nimport net.wiringbits.repositories.*\nimport play.api.db.Database\n\ncase class RepositoryComponents(\n    database: Database,\n    users: UsersRepository,\n    userTokens: UserTokensRepository,\n    userLogs: UserLogsRepository,\n    backgroundJobs: BackgroundJobsRepository\n)\n"
  },
  {
    "path": "server/src/test/scala/net/wiringbits/core/RepositorySpec.scala",
    "content": "package net.wiringbits.core\n\nimport net.wiringbits.config.UserTokensConfig\nimport net.wiringbits.repositories.*\nimport org.scalatest.concurrent.ScalaFutures.*\nimport org.scalatest.wordspec.AnyWordSpec\nimport utils.Executors\n\nimport java.time.Clock\nimport scala.concurrent.ExecutionContext\nimport scala.concurrent.duration.DurationInt\n\ntrait RepositorySpec extends AnyWordSpec with PostgresSpec {\n  implicit val patienceConfig: PatienceConfig = PatienceConfig(30.seconds, 1.second)\n  implicit val executionContext: ExecutionContext = Executors.globalEC\n\n  def withRepositories[T](clock: Clock = Clock.systemUTC)(runTest: RepositoryComponents => T): T = withDatabase { db =>\n    val users = new UsersRepository(db, UserTokensConfig(1.hour, 1.hour, \"secret\"))(Executors.databaseEC, clock)\n    val userTokens = new UserTokensRepository(db)(Executors.databaseEC)\n    val userLogs = new UserLogsRepository(db)(Executors.databaseEC)\n    val backgroundJobs = new BackgroundJobsRepository(db)(Executors.databaseEC, clock)\n    val components =\n      RepositoryComponents(\n        db,\n        users,\n        userTokens,\n        userLogs,\n        backgroundJobs\n      )\n    runTest(components)\n  }\n}\n"
  },
  {
    "path": "server/src/test/scala/net/wiringbits/repositories/BackgroundJobsRepositorySpec.scala",
    "content": "package net.wiringbits.repositories\n\nimport org.apache.pekko.actor.ActorSystem\nimport org.apache.pekko.stream.scaladsl.*\nimport net.wiringbits.common.models.Email\nimport net.wiringbits.core.RepositorySpec\nimport net.wiringbits.models.jobs.{BackgroundJobPayload, BackgroundJobStatus, BackgroundJobType}\nimport net.wiringbits.repositories.daos.{BackgroundJobDAO, backgroundJobParser}\nimport net.wiringbits.repositories.models.BackgroundJobData\nimport org.scalatest.BeforeAndAfterAll\nimport org.scalatest.OptionValues.*\nimport org.scalatest.concurrent.ScalaFutures.*\nimport org.scalatest.matchers.must.Matchers.*\nimport play.api.libs.json.Json\nimport utils.RepositoryUtils\n\nimport java.time.Instant\nimport java.util.UUID\n\nclass BackgroundJobsRepositorySpec extends RepositorySpec with BeforeAndAfterAll with RepositoryUtils {\n\n  // required to test the streaming operations\n  private implicit lazy val system: ActorSystem = ActorSystem(\"BackgroundJobsRepositorySpec\")\n\n  override def afterAll(): Unit = {\n    system.terminate().futureValue\n    super.afterAll()\n  }\n\n  \"streamPendingJobs\" should {\n\n    \"work (simple case)\" in withRepositories() { implicit repositories =>\n      val createRequest = createBackgroundJobData()\n\n      val result = repositories.backgroundJobs.streamPendingJobs.futureValue\n        .runWith(Sink.seq)\n        .futureValue\n\n      result.size must be(1)\n      val item = result.headOption.value\n      item.status must be(createRequest.status)\n      item.`type` must be(createRequest.`type`)\n      item.payload must be(Json.toJson(createRequest.payload))\n    }\n\n    \"only return pending jobs\" in withRepositories() { implicit repositories =>\n      val backgroundJobType = BackgroundJobType.SendEmail\n      val payload = backgroundJobPayload\n      val limit = 6\n      for (i <- 1 to limit) {\n        createBackgroundJobData(\n          backgroundJobType = backgroundJobType,\n          payload = payload,\n          status = if (i % 2) == 0 then BackgroundJobStatus.Success else BackgroundJobStatus.Pending\n        )\n      }\n      val response = repositories.backgroundJobs.streamPendingJobs.futureValue\n        .runWith(Sink.seq)\n        .futureValue\n      response.length must be(limit / 2)\n      response.foreach { x =>\n        x.status must be(BackgroundJobStatus.Pending)\n        x.`type` must be(backgroundJobType)\n        x.payload must be(Json.toJson(payload))\n      }\n    }\n\n    \"return no results\" in withRepositories() { repositories =>\n      val response = repositories.backgroundJobs.streamPendingJobs.futureValue\n        .runWith(Sink.seq)\n        .futureValue\n      response.isEmpty must be(true)\n    }\n  }\n\n  \"setStatusToFailed\" should {\n    \"work\" in withRepositories() { implicit repositories =>\n      val createRequest = createBackgroundJobData()\n\n      val failReason = \"test\"\n      repositories.backgroundJobs\n        .setStatusToFailed(createRequest.id, executeAt = Instant.now(), failReason = failReason)\n        .futureValue\n      val result = repositories.backgroundJobs.streamPendingJobs.futureValue\n        .runWith(Sink.seq)\n        .futureValue\n\n      result.size must be(1)\n      val item = result.headOption.value\n      item.id must be(createRequest.id)\n      item.status must be(BackgroundJobStatus.Failed)\n      item.statusDetails must be(Some(failReason))\n    }\n\n    \"fail if the job doesn't exists\" in withRepositories() { repositories =>\n      pending // TODO: setStatusToFailed must actually return an error because right now it succeeds\n\n      repositories.backgroundJobs\n        .setStatusToFailed(UUID.randomUUID(), executeAt = Instant.now(), failReason = \"test\")\n        .futureValue\n    }\n  }\n\n  \"setStatusToSuccess\" should {\n    \"work\" in withRepositories() { implicit repositories =>\n      val createRequest = createBackgroundJobData()\n\n      repositories.backgroundJobs.setStatusToSuccess(createRequest.id).futureValue\n\n      val result = repositories.backgroundJobs.streamPendingJobs.futureValue\n        .runWith(Sink.seq)\n        .futureValue\n      result.isEmpty must be(true)\n    }\n\n    \"fail if the notification doesn't exists\" in withRepositories() { repositories =>\n      pending // TODO: setStatusToFailed must actually return an error because right now it succeeds\n      repositories.backgroundJobs.setStatusToSuccess(UUID.randomUUID()).futureValue\n    }\n  }\n}\n"
  },
  {
    "path": "server/src/test/scala/net/wiringbits/repositories/UserLogsRepositorySpec.scala",
    "content": "package net.wiringbits.repositories\n\nimport net.wiringbits.core.RepositorySpec\nimport org.scalatest.concurrent.ScalaFutures.*\nimport org.scalatest.matchers.must.Matchers.*\nimport utils.RepositoryUtils\n\nimport java.util.UUID\n\nclass UserLogsRepositorySpec extends RepositorySpec with RepositoryUtils {\n  \"create\" should {\n    \"work\" in withRepositories() { implicit repositories =>\n      val request = createUser().futureValue\n\n      createUserLog(request.id).futureValue\n    }\n\n    \"fail if the user doesn't exists\" in withRepositories() { implicit repositories =>\n      val ex = intercept[RuntimeException] {\n        createUserLog(UUID.randomUUID()).futureValue\n      }\n      ex.getCause.getMessage must startWith(\n        s\"\"\"ERROR: insert or update on table \"user_logs\" violates foreign key constraint \"user_logs_users_fk\"\"\"\"\n      )\n    }\n  }\n\n  \"create(userId, message)\" should {\n    \"work\" in withRepositories() { implicit repositories =>\n      val request = createUser().futureValue\n\n      createUserLog(request.id, \"test\").futureValue\n    }\n\n    \"fail if the user doesn't exists\" in withRepositories() { implicit repositories =>\n      val ex = intercept[RuntimeException] {\n        createUserLog(UUID.randomUUID(), \"test\").futureValue\n      }\n      ex.getCause.getMessage must startWith(\n        s\"\"\"ERROR: insert or update on table \"user_logs\" violates foreign key constraint \"user_logs_users_fk\"\"\"\"\n      )\n    }\n  }\n\n  \"logs\" should {\n    \"return every log\" in withRepositories() { implicit repositories =>\n      val request = createUser().futureValue\n\n      val message = \"test\"\n      val expected = 3\n      (1 to expected).foreach { _ =>\n        createUserLog(request.id, message).futureValue\n      }\n\n      val response = repositories.userLogs.logs(request.id).futureValue\n      // Creating a user generates a user log. 3 + 1\n      response.length must be(expected + 1)\n    }\n\n    \"return no results\" in withRepositories() { repositories =>\n      val response = repositories.userLogs.logs(UUID.randomUUID()).futureValue\n      response.isEmpty must be(true)\n    }\n  }\n}\n"
  },
  {
    "path": "server/src/test/scala/net/wiringbits/repositories/UserTokensRepositorySpec.scala",
    "content": "package net.wiringbits.repositories\n\nimport net.wiringbits.core.RepositorySpec\nimport org.scalatest.OptionValues.convertOptionToValuable\nimport org.scalatest.concurrent.ScalaFutures.*\nimport org.scalatest.matchers.must.Matchers.*\nimport utils.RepositoryUtils\n\nimport java.util.UUID\n\nclass UserTokensRepositorySpec extends RepositorySpec with RepositoryUtils {\n  \"create\" should {\n    \"work\" in withRepositories() { implicit repositories =>\n      val request = createUser().futureValue\n\n      createToken(request.id).futureValue\n    }\n\n    \"fail when the user doesn't exists\" in withRepositories() { implicit repositories =>\n      val ex = intercept[RuntimeException] {\n        createToken(UUID.randomUUID()).futureValue\n      }\n      ex.getCause.getMessage must startWith(\n        s\"\"\"ERROR: insert or update on table \"user_tokens\" violates foreign key constraint \"user_tokens_user_id_fk\"\"\"\"\n      )\n    }\n  }\n\n  \"find(userId)\" should {\n    \"return the user token\" in withRepositories() { implicit repositories =>\n      val request = createUser().futureValue\n\n      val tokenRequest = createToken(request.id).futureValue\n\n      val maybe = repositories.userTokens.find(request.id).futureValue\n      val response = maybe.headOption.value\n      response.token must be(tokenRequest.token)\n      response.tokenType must be(tokenRequest.tokenType)\n      response.id must be(tokenRequest.id)\n    }\n\n    \"return no results when the user doesn't exists\" in withRepositories() { repositories =>\n      val response = repositories.userTokens.find(UUID.randomUUID()).futureValue\n      response.isEmpty must be(true)\n    }\n  }\n\n  \"find(userId, token)\" should {\n    \"return the user token\" in withRepositories() { implicit repositories =>\n      val request = createUser().futureValue\n\n      val tokenRequest = createToken(request.id).futureValue\n\n      val response = repositories.userTokens.find(request.id, tokenRequest.token).futureValue\n      response.isDefined must be(true)\n    }\n\n    \"return no results when the user doesn't exists\" in withRepositories() { repositories =>\n      val response = repositories.userTokens.find(UUID.randomUUID(), \"test\").futureValue\n      response.isEmpty must be(true)\n    }\n  }\n\n  \"delete\" should {\n    \"work\" in withRepositories() { implicit repositories =>\n      val request = createUser().futureValue\n\n      val maybe = repositories.userTokens.find(request.id).futureValue\n      val tokenId = maybe.headOption.value.id\n\n      repositories.userTokens.delete(tokenId = tokenId, userId = request.id).futureValue\n\n      val response = repositories.userTokens.find(request.id).futureValue\n      response.isEmpty must be(true)\n    }\n  }\n}\n"
  },
  {
    "path": "server/src/test/scala/net/wiringbits/repositories/UsersRepositorySpec.scala",
    "content": "package net.wiringbits.repositories\n\nimport org.apache.pekko.actor.ActorSystem\nimport org.apache.pekko.stream.scaladsl.Sink\nimport net.wiringbits.common.models.{Email, Name}\nimport net.wiringbits.core.RepositorySpec\nimport net.wiringbits.repositories.models.User\nimport net.wiringbits.util.EmailMessage\nimport org.scalatest.BeforeAndAfterAll\nimport org.scalatest.OptionValues.*\nimport org.scalatest.concurrent.ScalaFutures.*\nimport org.scalatest.matchers.must.Matchers.*\nimport utils.RepositoryUtils\n\nimport java.util.UUID\n\nclass UsersRepositorySpec extends RepositorySpec with BeforeAndAfterAll with RepositoryUtils {\n\n  // required to test the streaming operations\n  private implicit lazy val system: ActorSystem = ActorSystem(\"UserRepositorySpec\")\n\n  override def afterAll(): Unit = {\n    system.terminate().futureValue\n    super.afterAll()\n  }\n\n  \"create\" should {\n    \"work\" in withRepositories() { implicit repositories =>\n      createUser().futureValue\n    }\n\n    \"create a token for verifying the email\" in withRepositories() { implicit repositories =>\n      val request = createUser().futureValue\n\n      val response = repositories.userTokens.find(request.id).futureValue\n      response.nonEmpty must be(true)\n    }\n\n    \"fail when the id already exists\" in withRepositories() { implicit repositories =>\n      val request = createUser().futureValue\n      val ex = intercept[RuntimeException] {\n        repositories.users.create(request.copy(email = Email.trusted(\"email2@wiringbits.net\"))).futureValue\n      }\n      // TODO: This should be a better message\n      ex.getCause.getMessage must startWith(\n        \"\"\"ERROR: duplicate key value violates unique constraint \"users_user_id_pk\"\"\"\"\n      )\n    }\n\n    \"fail when the email already exists\" in withRepositories() { implicit repositories =>\n      val request = createUser().futureValue\n      val ex = intercept[RuntimeException] {\n        repositories.users.create(request.copy(id = UUID.randomUUID())).futureValue\n      }\n      // TODO: This should be a better message\n      ex.getCause.getMessage must startWith(\n        \"\"\"ERROR: duplicate key value violates unique constraint \"users_email_unique\"\"\"\"\n      )\n    }\n  }\n\n  \"all\" should {\n    \"return the existing users\" in withRepositories() { implicit repositories =>\n      val expected = 3\n      for (i <- 1 to expected) {\n        createUser(email = Email.trusted(s\"test$i@wiringbits.net\")).futureValue\n      }\n\n      val response = repositories.users.all().futureValue\n      response.length must be(3)\n    }\n\n    \"return no users\" in withRepositories() { repositories =>\n      val response = repositories.users.all().futureValue\n      response.isEmpty must be(true)\n    }\n  }\n\n  \"find(email)\" should {\n    \"return a user when the email exists\" in withRepositories() { implicit repositories =>\n      val request = createUser().futureValue\n\n      val response = repositories.users.find(request.email).futureValue\n      response.value.email must be(request.email)\n      response.value.id must be(request.id)\n      response.value.hashedPassword must be(request.hashedPassword)\n    }\n\n    \"return a user when the email exists (case insensitive match)\" in withRepositories() { implicit repositories =>\n      val request = createUser().futureValue\n\n      val email = Email.trusted(request.email.string.toUpperCase)\n      val response = repositories.users.find(email).futureValue\n      response.isDefined must be(true)\n    }\n\n    \"return no result when the email doesn't exists\" in withRepositories() { repositories =>\n      val email = Email.trusted(\"hello@wiringbits.net\")\n      val response = repositories.users.find(email).futureValue\n      response.isEmpty must be(true)\n    }\n  }\n\n  \"find(id)\" should {\n    \"return a user when the id exists\" in withRepositories() { implicit repositories =>\n      val request = createUser().futureValue\n\n      val response = repositories.users.find(request.id).futureValue\n      response.value.email must be(request.email)\n      response.value.id must be(request.id)\n      response.value.hashedPassword must be(request.hashedPassword)\n    }\n\n    \"return no result when the id doesn't exists\" in withRepositories() { repositories =>\n      val id = UUID.randomUUID()\n      val response = repositories.users.find(id).futureValue\n      response.isEmpty must be(true)\n    }\n  }\n\n  \"update\" should {\n    \"update an existing user\" in withRepositories() { implicit repositories =>\n      val request = createUser().futureValue\n\n      val newName = Name.trusted(\"Test\")\n      repositories.users.update(request.id, newName).futureValue\n\n      val response = repositories.users.find(request.id).futureValue\n      response.value.name must be(newName)\n      response.value.email must be(request.email)\n    }\n\n    \"fail when the user doesn't exist\" in withRepositories() { repositories =>\n      val id = UUID.randomUUID()\n      val newName = Name.trusted(\"Test\")\n      val ex = intercept[RuntimeException] {\n        repositories.users.update(id, newName).futureValue\n      }\n      ex.getCause.getMessage must startWith(\n        \"\"\"ERROR: insert or update on table \"user_logs\" violates foreign key constraint \"user_logs_users_fk\"\"\"\"\n      )\n    }\n  }\n\n  \"updatePassword\" should {\n    \"update the password for an existing user\" in withRepositories() { implicit repositories =>\n      val request = createUser().futureValue\n\n      val newPassword = \"test\"\n      repositories.users.updatePassword(request.id, newPassword, EmailMessage.updatePassword(request.name)).futureValue\n\n      val response = repositories.users.find(request.id).futureValue\n      response.value.hashedPassword must be(newPassword)\n    }\n\n    \"produce a notification for the user\" in withRepositories() { implicit repositories =>\n      val request = createUser().futureValue\n\n      val newPassword = \"test\"\n      repositories.users.updatePassword(request.id, newPassword, EmailMessage.updatePassword(request.name)).futureValue\n\n      val response = repositories.backgroundJobs.streamPendingJobs.futureValue\n        .runWith(Sink.seq)\n        .futureValue\n      response.length must be(1)\n    }\n\n    \"fail when the user doesn't exist\" in withRepositories() { repositories =>\n      val name = Name.trusted(\"test\")\n      val ex = intercept[RuntimeException] {\n        repositories.users.updatePassword(UUID.randomUUID(), \"test\", EmailMessage.updatePassword(name)).futureValue\n      }\n      ex.getCause.getMessage must startWith(\n        \"\"\"ERROR: insert or update on table \"user_logs\" violates foreign key constraint \"user_logs_users_fk\"\"\"\"\n      )\n    }\n  }\n\n  \"verify\" should {\n    \"verify a user given a token\" in withRepositories() { implicit repositories =>\n      val request = createUser().futureValue\n      repositories.users.verify(request.id, UUID.randomUUID(), EmailMessage.confirm(request.name)).futureValue\n\n      val response = repositories.users.find(request.id).futureValue\n      response.value.verifiedOn.isDefined must be(true)\n    }\n\n    \"produce a notification for the user\" in withRepositories() { implicit repositories =>\n      val request = createUser().futureValue\n      repositories.users.verify(request.id, UUID.randomUUID(), EmailMessage.confirm(request.name)).futureValue\n\n      val response = repositories.backgroundJobs.streamPendingJobs.futureValue\n        .runWith(Sink.seq)\n        .futureValue\n      response.length must be(1)\n    }\n\n    \"fail when the user doesn't exist\" in withRepositories() { repositories =>\n      val name = Name.trusted(\"test\")\n      val ex = intercept[RuntimeException] {\n        repositories.users.verify(UUID.randomUUID(), UUID.randomUUID(), EmailMessage.confirm(name)).futureValue\n      }\n      ex.getCause.getMessage must startWith(\n        \"\"\"ERROR: insert or update on table \"user_logs\" violates foreign key constraint \"user_logs_users_fk\"\"\"\"\n      )\n    }\n  }\n\n  \"resetPassword\" should {\n    \"update the password\" in withRepositories() { implicit repositories =>\n      val request = createUser().futureValue\n\n      val newPassword = \"test\"\n      repositories.users.resetPassword(request.id, newPassword, EmailMessage.resetPassword(request.name)).futureValue\n\n      val response = repositories.users.find(request.id).futureValue\n      response.value.hashedPassword must be(newPassword)\n    }\n\n    \"produce a notification for the user\" in withRepositories() { implicit repositories =>\n      val request = createUser().futureValue\n\n      val newPassword = \"test\"\n      repositories.users.resetPassword(request.id, newPassword, EmailMessage.resetPassword(request.name)).futureValue\n\n      val response = repositories.backgroundJobs.streamPendingJobs.futureValue\n        .runWith(Sink.seq)\n        .futureValue\n      response.length must be(1)\n    }\n\n    \"fail when the user doesn't exist\" in withRepositories() { repositories =>\n      val name = Name.trusted(\"test\")\n      val ex = intercept[RuntimeException] {\n        repositories.users.resetPassword(UUID.randomUUID(), \"test\", EmailMessage.resetPassword(name)).futureValue\n      }\n      ex.getCause.getMessage must startWith(\n        \"\"\"ERROR: insert or update on table \"user_logs\" violates foreign key constraint \"user_logs_users_fk\"\"\"\"\n      )\n    }\n  }\n}\n"
  },
  {
    "path": "server/src/test/scala/net/wiringbits/util/DelayGeneratorSpec.scala",
    "content": "package net.wiringbits.util\n\nimport org.scalatest.matchers.must.Matchers.{be, must}\nimport org.scalatest.wordspec.AnyWordSpec\n\nclass DelayGeneratorSpec extends AnyWordSpec {\n\n  \"createDelay\" should {\n    \"create an exponential sequence in a linear sequence of numbers\" in {\n      val expected = List(1, 2, 4, 8, 16)\n\n      val response = expected.indices.map(x => DelayGenerator.createDelay(x)).toList\n\n      response must be(expected)\n    }\n  }\n}\n"
  },
  {
    "path": "server/src/test/scala/net/wiringbits/util/TokensHelperSpec.scala",
    "content": "package net.wiringbits.util\n\nimport org.scalatest.matchers.must.Matchers.{be, empty, must, mustNot}\nimport org.scalatest.wordspec.AnyWordSpec\n\nimport java.util.UUID\n\nclass TokensHelperSpec extends AnyWordSpec {\n\n  \"doHMACSHA1\" should {\n    \"create a valid hmac\" in {\n      val uuid = UUID.randomUUID()\n      val secretKey = \"test\"\n      val hmac = TokensHelper.doHMACSHA1(value = uuid.toString.getBytes, secretKey = secretKey)\n\n      hmac mustNot be(empty)\n    }\n  }\n\n  \"isSignatureValid\" should {\n    \"return true when the data doesn't changes\" in {\n      val uuid = UUID.randomUUID()\n      val secretKey = \"test\"\n      val hmac = TokensHelper.doHMACSHA1(value = uuid.toString.getBytes, secretKey = secretKey)\n\n      TokensHelper.isSignatureValid(tokensSecret = secretKey, digest = hmac, data = uuid.toString.getBytes) must be(\n        true\n      )\n    }\n\n    \"return false when the data changes\" in {\n      val secretKey = \"test\"\n      val hmac = TokensHelper.doHMACSHA1(value = UUID.randomUUID.toString.getBytes, secretKey = secretKey)\n\n      TokensHelper.isSignatureValid(\n        tokensSecret = secretKey,\n        digest = hmac,\n        data = UUID.randomUUID.toString.getBytes\n      ) must be(false)\n    }\n  }\n\n}\n"
  },
  {
    "path": "server/src/test/scala/utils/Executors.scala",
    "content": "package utils\n\nimport net.wiringbits.executors.DatabaseExecutionContext\n\nimport scala.concurrent.ExecutionContext\n\nobject Executors {\n\n  implicit val globalEC: ExecutionContext = scala.concurrent.ExecutionContext.global\n\n  implicit val databaseEC: DatabaseExecutionContext = new DatabaseExecutionContext {\n    override def execute(runnable: Runnable): Unit = globalEC.execute(runnable)\n\n    override def reportFailure(cause: Throwable): Unit = globalEC.reportFailure(cause)\n  }\n}\n"
  },
  {
    "path": "server/src/test/scala/utils/LoginUtils.scala",
    "content": "package utils\n\nimport net.wiringbits.api.ApiClient\nimport net.wiringbits.api.models.auth.Login\nimport net.wiringbits.api.models.users.{CreateUser, VerifyEmail}\nimport net.wiringbits.common.models.*\nimport net.wiringbits.util.TokenGenerator\nimport org.mockito.Mockito.*\n\nimport java.util.UUID\nimport scala.annotation.unused\nimport scala.concurrent.{ExecutionContext, Future}\n\ntrait LoginUtils {\n  def createUser(\n      nameMaybe: Option[Name] = None,\n      emailMaybe: Option[Email] = None,\n      passwordMaybe: Option[Password] = None\n  )(using\n      @unused ec: ExecutionContext,\n      client: ApiClient\n  ): Future[CreateUser.Response] = {\n    val request = CreateUser.Request(\n      name = nameMaybe.getOrElse(Name.trusted(\"wiringbits\")),\n      email = emailMaybe.getOrElse(Email.trusted(s\"test${UUID.randomUUID()}@email.com\")),\n      password = passwordMaybe.getOrElse(Password.trusted(\"test123...\")),\n      captcha = Captcha.trusted(\"test\")\n    )\n\n    client.createUser(request)\n  }\n\n  def createVerifyLoginUser(\n      tokenGenerator: TokenGenerator,\n      nameMaybe: Option[Name] = None,\n      emailMaybe: Option[Email] = None,\n      passwordMaybe: Option[Password] = None\n  )(using @unused ec: ExecutionContext, client: ApiClient): Future[Login.Response] = {\n    val verificationToken = UUID.randomUUID()\n    when(tokenGenerator.next()).thenReturn(verificationToken)\n\n    for {\n      user <- createUser(nameMaybe, emailMaybe, passwordMaybe)\n      _ <- client.verifyEmail(VerifyEmail.Request(UserToken(user.id, verificationToken)))\n      loginRequest = Login.Request(\n        email = user.email,\n        password = passwordMaybe.getOrElse(Password.trusted(\"test123...\")),\n        captcha = Captcha.trusted(\"test\")\n      )\n      response <- client.login(loginRequest)\n    } yield response\n  }\n}\n"
  },
  {
    "path": "server/src/test/scala/utils/RepositoryUtils.scala",
    "content": "package utils\n\nimport net.wiringbits.common.models.{Email, Name}\nimport net.wiringbits.core.RepositoryComponents\nimport net.wiringbits.models.jobs.{BackgroundJobPayload, BackgroundJobStatus, BackgroundJobType}\nimport net.wiringbits.repositories.daos.BackgroundJobDAO\nimport net.wiringbits.repositories.models.{BackgroundJobData, User, UserLog, UserToken, UserTokenType}\nimport org.scalatest.concurrent.ScalaFutures.*\n\nimport java.time.Instant\nimport java.time.temporal.ChronoUnit\nimport java.util.UUID\nimport scala.annotation.unused\nimport scala.concurrent.{ExecutionContext, Future}\n\ntrait RepositoryUtils {\n  val backgroundJobPayload: BackgroundJobPayload.SendEmail =\n    BackgroundJobPayload.SendEmail(Email.trusted(\"sample@wiringbits.net\"), subject = \"Test message\", body = \"it works\")\n\n  def createBackgroundJobData(\n      id: UUID = UUID.randomUUID(),\n      backgroundJobType: BackgroundJobType = BackgroundJobType.SendEmail,\n      status: BackgroundJobStatus = BackgroundJobStatus.Pending,\n      payload: BackgroundJobPayload = backgroundJobPayload\n  )(using repositories: RepositoryComponents): BackgroundJobData.Create = {\n    val createRequest = BackgroundJobData.Create(\n      id = id,\n      `type` = backgroundJobType,\n      payload = payload,\n      status = status,\n      executeAt = Instant.now(),\n      createdAt = Instant.now(),\n      updatedAt = Instant.now()\n    )\n\n    repositories.database.withConnection { implicit conn =>\n      BackgroundJobDAO.create(createRequest)\n    }\n\n    createRequest\n  }\n\n  def createUser(\n      email: Email = Email.trusted(\"hello@wiringbits.net\")\n  )(using repository: RepositoryComponents)(using @unused ec: ExecutionContext): Future[User.CreateUser] = {\n    val createRequest = User.CreateUser(\n      id = UUID.randomUUID(),\n      email = email,\n      name = Name.trusted(\"Sample\"),\n      hashedPassword = \"password\",\n      verifyEmailToken = \"token\"\n    )\n\n    for {\n      _ <- repository.users.create(createRequest)\n    } yield createRequest\n  }\n\n  def createUserLog(\n      userId: UUID\n  )(using repository: RepositoryComponents)(using @unused ec: ExecutionContext): Future[UserLog.CreateUserLog] = {\n    val createRequest =\n      UserLog.CreateUserLog(userLogId = UUID.randomUUID(), userId = userId, message = \"test\")\n\n    for {\n      _ <- repository.userLogs.create(createRequest)\n    } yield createRequest\n  }\n\n  def createUserLog(\n      userId: UUID,\n      message: String\n  )(using repository: RepositoryComponents): Future[Unit] = {\n    repository.userLogs.create(userId, message)\n  }\n\n  def createToken(\n      userId: UUID\n  )(using @unused ec: ExecutionContext, repository: RepositoryComponents): Future[UserToken.Create] = {\n    val tokenRequest =\n      UserToken.Create(\n        id = UUID.randomUUID(),\n        token = \"test\",\n        tokenType = UserTokenType.ResetPassword,\n        createdAt = Instant.now(),\n        expiresAt = Instant.now.plus(2, ChronoUnit.DAYS),\n        userId = userId\n      )\n\n    for {\n      _ <- repository.userTokens.create(tokenRequest)\n    } yield tokenRequest\n  }\n}\n"
  },
  {
    "path": "web/src/main/js/index.css",
    "content": "html,\nbody,\n#root {\n    min-height: 100vh;\n    display: flex;\n    flex-direction: column;\n}"
  },
  {
    "path": "web/src/main/js/index.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n\n<head>\n  <meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\">\n  <meta charset=\"utf-8\">\n  <meta name=\"viewport\" content=\"width=device-width, initial-scale=1, shrink-to-fit=no\">\n  <meta name=\"msapplication-TileColor\" content=\"#da532c\">\n  <meta name=\"theme-color\" content=\"#000000\">\n  <title>Wiringbits Web App Template</title>\n  <base href=\"/\">\n  <link rel=\"icon\" type=\"image/x-icon\" href=\"favicon.ico\">\n  <link rel=\"apple-touch-icon\" sizes=\"180x180\" href=\"assets/apple-touch-icon.png\">\n  <link rel=\"icon\" type=\"image/png\" sizes=\"48x48\" href=\"favicon.ico\">\n  <link rel=\"manifest\" href=\"assets/site.webmanifest\">\n  <link rel=\"mask-icon\" href=\"assets/safari-pinned-tab.svg\" color=\"#5bbad5\">\n  <link rel=\"shortcut icon\" href=\"favicon.ico\">\n  <link rel=\"stylesheet\" href=\"https://cdnjs.cloudflare.com/ajax/libs/animate.css/4.1.1/animate.min.css\" integrity=\"sha512-c42qTSw/wPZ3/5LBzD+Bw5f7bSF2oxou6wEb+I/lqeaKV5FDIfMvvRp772y4jcJLKuGUOpbJMdg/BTl50fJYAw==\" crossorigin=\"anonymous\" />\n</head>\n\n<body>\n  <noscript>\n    You need to enable JavaScript to run this app.\n  </noscript>\n  <div id=\"root\"></div>\n  <script type=\"text/javascript\" src=\"wiringbits-web-fastopt-bundle.js\"></script>\n\n</body>\n\n</html>\n"
  },
  {
    "path": "web/src/main/scala/net/wiringbits/API.scala",
    "content": "package net.wiringbits\n\nimport net.wiringbits.api.ApiClient\nimport net.wiringbits.services.StorageService\nimport org.scalajs.macrotaskexecutor.MacrotaskExecutor.Implicits.*\nimport sttp.client3.SttpBackend\n\nimport scala.concurrent.Future\n\ncase class API(client: ApiClient, storage: StorageService)\n\nobject API {\n\n  // allows overriding the server url\n  private val apiUrl = {\n    net.wiringbits.BuildInfo.apiUrl.filter(_.nonEmpty).getOrElse {\n      \"http://localhost:8080/api\"\n    }\n  }\n\n  def apply(): API = {\n    println(s\"Server API expected at: $apiUrl\")\n\n    implicit val sttpBackend: SttpBackend[Future, _] = sttp.client3.FetchBackend()\n    val client = new ApiClient(ApiClient.Config(apiUrl))\n    val storage = new StorageService\n\n    API(client, storage)\n  }\n}\n"
  },
  {
    "path": "web/src/main/scala/net/wiringbits/App.scala",
    "content": "package net.wiringbits\n\nimport com.olvind.mui.muiMaterial.components.ThemeProvider\nimport com.olvind.mui.muiMaterial.components as mui\nimport net.wiringbits.components.AppSplash\nimport net.wiringbits.webapp.utils.slinkyUtils.facades.reactrouterdom.BrowserRouter\nimport slinky.core.{FunctionalComponent, KeyAddingStage}\nimport slinky.core.facade.ReactElement\n\nobject App {\n  def apply(ctx: AppContext): KeyAddingStage =\n    component(Props(ctx = ctx))\n\n  case class Props(ctx: AppContext)\n\n  val component: FunctionalComponent[Props] = FunctionalComponent[Props] { props =>\n    ThemeProvider(AppTheme.value)(\n      mui.ThemeProvider(AppTheme.value)(\n        mui.CssBaseline(),\n        BrowserRouter(basename = \"\")(\n          AppSplash(props.ctx)(AppRouter(props.ctx): ReactElement)\n        )\n      )\n    )\n  }\n}\n"
  },
  {
    "path": "web/src/main/scala/net/wiringbits/AppContext.scala",
    "content": "package net.wiringbits\n\nimport monix.reactive.subjects.Var\nimport net.wiringbits.common.models.Email\nimport net.wiringbits.core.I18nLang\nimport net.wiringbits.models.{AuthState, User}\n\nimport scala.concurrent.ExecutionContext\nimport scala.language.postfixOps\n\ncase class AppContext(\n    api: API,\n    $auth: Var[AuthState],\n    $lang: Var[I18nLang],\n    contactEmail: Email,\n    contactPhone: String,\n    executionContext: ExecutionContext\n) {\n\n  // TODO: This is hacky but it works while preventing to pollute all components from depending on the Texts\n  //       still, it would be ideal to keep a Var with the current Texts instance\n  def texts(lang: I18nLang): I18nMessages = new I18nMessages(lang)\n\n  def loggedIn(user: User): Unit = {\n    $auth := AuthState.Authenticated(user)\n  }\n\n  def loggedOut(): Unit = {\n    $auth := AuthState.Unauthenticated\n  }\n\n  def switchLang(newLang: I18nLang): Unit = {\n    api.storage.saveLang(newLang)\n    $lang := newLang\n  }\n}\n"
  },
  {
    "path": "web/src/main/scala/net/wiringbits/AppRouter.scala",
    "content": "package net.wiringbits\n\nimport net.wiringbits.components.pages.*\nimport net.wiringbits.components.widgets.{AppBar, Footer}\nimport net.wiringbits.core.ReactiveHooks\nimport net.wiringbits.models.{AuthState, User}\nimport net.wiringbits.webapp.utils.slinkyUtils.components.core.widgets.Scaffold\nimport net.wiringbits.webapp.utils.slinkyUtils.facades.reactrouterdom.{Redirect, Route, Switch}\nimport slinky.core.facade.ReactElement\nimport slinky.core.{FunctionalComponent, KeyAddingStage}\n\nimport scala.concurrent.ExecutionContext\nimport scala.util.{Failure, Success}\n\nobject AppRouter {\n  def apply(ctx: AppContext): KeyAddingStage =\n    component(Props(ctx = ctx))\n\n  case class Props(ctx: AppContext)\n\n  private def route(path: String, ctx: AppContext)(child: => ReactElement): ReactElement = {\n    Route(path = path, exact = true)(\n      Scaffold(\n        appbar = Some(AppBar(ctx)),\n        body = child,\n        footer = Some(Footer(ctx))\n      )\n    )\n  }\n\n  val component: FunctionalComponent[Props] = FunctionalComponent[Props] { props =>\n    implicit val ec: ExecutionContext = props.ctx.executionContext\n    val auth = ReactiveHooks.useDistinctValue(props.ctx.$auth)\n    val home = route(\"/\", props.ctx)(HomePage(props.ctx))\n    val about = route(\"/about\", props.ctx)(AboutPage(props.ctx))\n    val signIn = route(\"/signin\", props.ctx)(SignInPage(props.ctx))\n    val signUp = route(\"/signup\", props.ctx)(SignUpPage(props.ctx))\n    val email = route(\"/verify-email\", props.ctx)(VerifyEmailPage(props.ctx))\n    val emailCode = route(\"/verify-email/:emailCode\", props.ctx)(VerifyEmailWithTokenPage(props.ctx))\n    val forgotPassword = route(\"/forgot-password\", props.ctx)(ForgotPasswordPage(props.ctx))\n    val resetPassword = route(\"/reset-password/:resetPasswordCode\", props.ctx)(ResetPasswordPage(props.ctx))\n    val resendVerifyEmail = route(\"/resend-verify-email\", props.ctx)(ResendVerifyEmailPage(props.ctx))\n\n    def dashboard(user: User) = route(\"/dashboard\", props.ctx)(DashboardPage(props.ctx, user))\n    def me(user: User) = route(\"/me\", props.ctx)(UserEditPage(props.ctx, user))\n    val signOut = route(\"/signout\", props.ctx) {\n      props.ctx.api.client.logout.onComplete {\n        case Success(_) =>\n          props.ctx.loggedOut()\n          println(\"Logged out successfully\")\n\n        case Failure(exception) =>\n          println(s\"Failed to log out: ${exception.getMessage}\")\n      }\n\n      Redirect(\"/\")\n    }\n\n    val catchAllRoute = Route(path = \"*\")(render = Redirect(\"/\"))\n\n    auth match {\n      case AuthState.Unauthenticated =>\n        Switch(\n          home,\n          about,\n          signIn,\n          signUp,\n          email,\n          emailCode,\n          forgotPassword,\n          resetPassword,\n          resendVerifyEmail,\n          catchAllRoute\n        )\n\n      case AuthState.Authenticated(user) =>\n        Switch(home, me(user), dashboard(user), about, signOut, catchAllRoute)\n    }\n  }\n}\n"
  },
  {
    "path": "web/src/main/scala/net/wiringbits/AppTheme.scala",
    "content": "package net.wiringbits\n\nimport com.olvind.mui.muiMaterial.stylesCreateThemeMod.ThemeOptions\nimport com.olvind.mui.muiMaterial.stylesCreateThemeMod.Theme\nimport com.olvind.mui.muiMaterial.stylesCreatePaletteMod.SimplePaletteColorOptions\nimport com.olvind.mui.muiMaterial.stylesCreatePaletteMod.PaletteOptions\nimport com.olvind.mui.muiMaterial.stylesCreateTypographyMod.TypographyOptions\nimport com.olvind.mui.muiMaterial.stylesMod.{createMuiTheme, createTheme}\nimport com.olvind.mui.muiMaterial.colorsMod as Colors\nimport com.olvind.mui.muiMaterial.components as mui\nimport com.olvind.mui.react.mod.CSSProperties\nimport com.olvind.mui.muiMaterial.mod.PropTypes.Color\nimport com.olvind.mui.muiIconsMaterial.components as muiIcons\nimport com.olvind.mui.csstype.mod.Property.{BoxSizing, FlexDirection, Position}\nimport com.olvind.mui.muiSystem.createThemeShapeMod.ShapeOptions\n\nobject AppTheme {\n  val primaryColor = Colors.teal.`500`\n  val secondaryColor = Colors.amber\n  val typography = TypographyOptions()\n  val borderRadius = 8\n\n  val value: Theme = createTheme(\n    ThemeOptions()\n      .setPalette(\n        PaletteOptions()\n          .setPrimary(SimplePaletteColorOptions(primaryColor))\n      )\n      .setTypography(typography)\n      .setShape(ShapeOptions().setBorderRadius(borderRadius))\n  )\n}\n"
  },
  {
    "path": "web/src/main/scala/net/wiringbits/I18nMessages.scala",
    "content": "package net.wiringbits\n\nimport net.wiringbits.common.models.Name\nimport net.wiringbits.core.I18nLang\nimport net.wiringbits.models.UserMenuOption\n\n// TODO: conditionaly render messages when we support more than 1 language\nclass I18nMessages(_lang: I18nLang) {\n\n  def appName = \"Wiringbits Web App Template\"\n  def appNameCopyright = s\"$appName ${java.time.ZonedDateTime.now.getYear}\"\n  def description =\n    \"While wiringbits is a company based in Culiacan, Mexico, there is no office, everyone works remotely. We strive for great quality on the software we built, and try to open source everything we can.\"\n  def profile = \"Profile\"\n  def home = \"Home\"\n  def dashboard = \"Dashboard\"\n  def user = \"User\"\n  def about = \"About\"\n  def signOut = \"Sign out\"\n  def signUp: String = \"Sign up\"\n  def signIn: String = \"Sign in\"\n  def loading: String = \"Loading\"\n  def welcome = \"Welcome\"\n\n  def completeData = \"Complete the necessary data\"\n\n  def contact = \"Contact\"\n  def phone = \"Phone\"\n\n  def name = \"Name\"\n  def email = \"Email\"\n  def password = \"Password\"\n  def oldPassword = \"Old password\"\n  def repeatPassword = \"Repeat password\"\n  def createdAt = \"Created at\"\n\n  def createAccount = \"Create account\"\n  def login = \"Login\"\n  def savePassword = \"Save password\"\n  def resetPassword = \"Reset password\"\n  def forgotYourPassword = \"Forgot your password?\"\n  def recoverYourPassword = \"Recover your password\"\n  def dontHaveAccountYet = \"You don't have an account yet?\"\n  def alreadyHaveAccount = \"Do you already have an account?\"\n  def enterNewPassword = \"Enter your new password\"\n  def save = \"Save\"\n  def recover = \"Recover\"\n  def recoverIt = \"Recover it\"\n  def reload = \"Reload\"\n  def logs = \"Logs\"\n  def resendEmail = \"Re-send email\"\n\n  def aboutPage = \"About page\"\n  def projectDetails = \"Add details about the project\"\n  def dashboardPage = \"Dashboard page\"\n  def homePage = \"Home page\"\n  def landingPageContent = \"The landing page content goes here\"\n\n  def verifyYourEmailAddress = \"Verify your email address\"\n  def successfulEmailVerification = \"Successful email verification\"\n  def failedEmailVerification = \"Failed email verification\"\n  def invalidVerificationToken = \"Invalid verification token\"\n  def goingToBeRedirected = \"You're going to be redirected\"\n  def emailHasBeenSent = \"An email has been sent to your email with a URL to verify your account.\"\n  def emailNotReceived = \"If you haven't received the email after a few minutes, please check your spam folder\"\n  def verifyingEmail = \"We're verifying your email\"\n  def waitAMomentPlease = \"Wait a moment, please\"\n  def completeTheCaptcha = \"Complete the captcha\"\n  def checkoutTheRepo = \"Checkout the repository!\"\n\n  def homePageDescription =\n    \"A reusable skeleton to build web applications in Scala/Scala.js, including user registration, login, and deployments.\"\n  def userProfile = \"User profile\"\n  def userProfileDescription = \"All the necessary code to create accounts, change passwords, update profile is there, \"\n  def tryIt = \"Try it.\"\n  def swaggerIntegration = \"Swagger integration\"\n  def swaggerIntegrationDescription =\n    \"The template already has the necessary boilerplate to expose the application's API through Swagger, \"\n  def consistentDataLoading = \"Consistent data loading\"\n  def consistentDataLoadingDescription =\n    \"Asynchronous data loading is consistent when using our `AsyncComponent`, for example:\"\n  def dataIsBeingLoaded = \"When the data is being loaded, a progress indicator is displayed:\"\n  def problemFetchingData = \"When there is a problem fetching data, we get an opportunity to retry:\"\n  def simpleToFollowArchitecture = \"A simple-to-follow architecture where tests are first class citizens\"\n  def simpleToFollowArchitectureDescription1 =\n    \"There is already an integration with GitHub Actions, and, there are already many integration tests to make sure that your APIs/Database work the way you expect them.\"\n  def simpleToFollowArchitectureDescription2 =\n    \"There are many layers which are easy to follow, which means, boarding new developers takes little effort.\"\n\n  def welcome(name: Name): String = {\n    s\"Welcome ${name.string}\"\n  }\n\n  def userMenuOption(menuOption: UserMenuOption): String = {\n    menuOption match {\n      case UserMenuOption.EditSummary => \"Summary\"\n      case UserMenuOption.EditPassword => \"Change password\"\n    }\n  }\n}\n"
  },
  {
    "path": "web/src/main/scala/net/wiringbits/Main.scala",
    "content": "package net.wiringbits\n\nimport monix.reactive.subjects.Var\nimport net.wiringbits.common.models.Email\nimport net.wiringbits.core.I18nLang\nimport net.wiringbits.models.AuthState\nimport net.wiringbits.webapp.utils.slinkyUtils.components.core.{ErrorBoundaryComponent, ErrorBoundaryInfo}\nimport org.scalajs.dom\nimport slinky.web.ReactDOM\n\nimport scala.scalajs.js\nimport scala.scalajs.js.annotation.JSImport\n\n@JSImport(\"js/index.css\", JSImport.Default)\n@js.native\nobject IndexCSS extends js.Object\n\nobject Main {\n  val css = IndexCSS\n\n  def main(argv: Array[String]): Unit = {\n    val scheduler = monix.execution.Scheduler.global\n    val $authState = Var[AuthState](AuthState.Unauthenticated)(scheduler)\n    val $lang = Var[I18nLang](I18nLang.English)(scheduler)\n    val ctx = AppContext(\n      API(),\n      $authState,\n      $lang,\n      Email.trusted(\"hello@wiringbits.net\"),\n      \"+52 (999) 9999 999\",\n      org.scalajs.macrotaskexecutor.MacrotaskExecutor\n    )\n    val app = ErrorBoundaryComponent(\n      ErrorBoundaryComponent.Props(\n        child = App(ctx),\n        renderError = e => ErrorBoundaryInfo(e)\n      )\n    )\n\n    ReactDOM.render(app, dom.document.getElementById(\"root\"))\n  }\n}\n"
  },
  {
    "path": "web/src/main/scala/net/wiringbits/components/AppSplash.scala",
    "content": "package net.wiringbits.components\n\nimport net.wiringbits.AppContext\nimport net.wiringbits.core.I18nHooks\nimport net.wiringbits.models.User\nimport net.wiringbits.webapp.utils.slinkyUtils.components.core.widgets.{Container, Subtitle, Title}\nimport org.scalajs.macrotaskexecutor.MacrotaskExecutor.Implicits.*\nimport slinky.core.FunctionalComponent\nimport slinky.core.facade.{Fragment, Hooks, ReactElement}\n\nimport scala.util.{Failure, Success}\n\nobject AppSplash {\n  def apply(ctx: AppContext)(child: ReactElement): ReactElement =\n    component(Props(ctx = ctx, child = child))\n\n  case class Props(ctx: AppContext, child: ReactElement)\n\n  val component: FunctionalComponent[Props] = FunctionalComponent[Props] { props =>\n    val texts = I18nHooks.useMessages(props.ctx.$lang)\n    val (initialized, setInitialized) = Hooks.useState(false)\n\n    Hooks.useEffect(\n      () => {\n        // load language\n        // TODO: It is ideal to detect the browser language when there is no language stored\n        props.ctx.api.storage\n          .findLang()\n          .foreach(lang => props.ctx.$lang := lang)\n\n        props.ctx.api.client.currentUser.onComplete {\n          case Success(res) =>\n            props.ctx.loggedIn(User(name = res.name, email = res.email))\n            setInitialized(true)\n\n          case Failure(ex) =>\n            println(\n              s\"Failed to get current user, we are either unauthenticated or the server had a problem: ${ex.getMessage}\"\n            )\n            setInitialized(true)\n        }\n\n      },\n      \"\"\n    )\n\n    if (initialized) {\n      Fragment(props.child)\n    } else {\n      Container(\n        flex = Some(1),\n        alignItems = Container.Alignment.center,\n        justifyContent = Container.Alignment.center,\n        child = Fragment(\n          Title(texts.appName),\n          Subtitle(texts.loading)\n        )\n      )\n    }\n  }\n}\n"
  },
  {
    "path": "web/src/main/scala/net/wiringbits/components/pages/AboutPage.scala",
    "content": "package net.wiringbits.components.pages\n\nimport com.olvind.mui.muiMaterial.components as mui\nimport net.wiringbits.AppContext\nimport net.wiringbits.core.I18nHooks\nimport net.wiringbits.webapp.utils.slinkyUtils.Utils.CSSPropertiesUtils\nimport net.wiringbits.webapp.utils.slinkyUtils.components.core.widgets.Container\nimport slinky.core.facade.Fragment\nimport slinky.core.{FunctionalComponent, KeyAddingStage}\nimport slinky.web.html.{alt, img, src, style}\n\nobject AboutPage {\n  def apply(ctx: AppContext): KeyAddingStage =\n    component(Props(ctx = ctx))\n\n  case class Props(ctx: AppContext)\n\n  private val styling = new CSSPropertiesUtils {\n    maxWidth = 300\n    maxHeight = 164\n  }\n\n  val component: FunctionalComponent[Props] = FunctionalComponent[Props] { props =>\n    val texts = I18nHooks.useMessages(props.ctx.$lang)\n\n    val wiringbitsImage =\n      img(src := \"/img/wiringbits-logo.png\", alt := \"wiringbits logo\", style := styling)\n    val repositoryLink = mui\n      .Link(texts.checkoutTheRepo)\n      .variant(\"h5\")\n      .color(\"inherit\")\n      .href(\"https://github.com/wiringbits/scala-webapp-template\")\n      .target(\"_blank\")\n\n    Container(\n      flex = Some(1),\n      alignItems = Container.Alignment.center,\n      margin = Container.EdgeInsets.top(48),\n      child = Fragment(\n        wiringbitsImage,\n        Container(\n          margin = Container.EdgeInsets.top(32),\n          child = repositoryLink\n        )\n      )\n    )\n  }\n}\n"
  },
  {
    "path": "web/src/main/scala/net/wiringbits/components/pages/DashboardPage.scala",
    "content": "package net.wiringbits.components.pages\n\nimport net.wiringbits.AppContext\nimport net.wiringbits.components.widgets.Logs\nimport net.wiringbits.core.I18nHooks\nimport net.wiringbits.models.User\nimport net.wiringbits.webapp.utils.slinkyUtils.components.core.widgets.{Container, Subtitle, Title}\nimport slinky.core.facade.Fragment\nimport slinky.core.{FunctionalComponent, KeyAddingStage}\n\nobject DashboardPage {\n  def apply(ctx: AppContext, user: User): KeyAddingStage =\n    component(Props(ctx = ctx, user = user))\n\n  case class Props(ctx: AppContext, user: User)\n\n  val component: FunctionalComponent[Props] = FunctionalComponent[Props] { props =>\n    val texts = I18nHooks.useMessages(props.ctx.$lang)\n\n    Fragment(\n      Container(\n        margin = Container.EdgeInsets.bottom(16),\n        child = Fragment(\n          Title(texts.dashboardPage),\n          Subtitle(texts.welcome(props.user.name))\n        )\n      ),\n      Logs(props.ctx, props.user)\n    )\n  }\n}\n"
  },
  {
    "path": "web/src/main/scala/net/wiringbits/components/pages/ForgotPasswordPage.scala",
    "content": "package net.wiringbits.components.pages\n\nimport com.olvind.mui.csstype.mod.Property.FlexDirection\nimport com.olvind.mui.muiMaterial.components as mui\nimport net.wiringbits.AppContext\nimport net.wiringbits.components.widgets.{AppCard, ForgotPasswordForm}\nimport net.wiringbits.core.I18nHooks\nimport net.wiringbits.webapp.utils.slinkyUtils.Utils.CSSPropertiesUtils\nimport net.wiringbits.webapp.utils.slinkyUtils.components.core.widgets.Container\nimport net.wiringbits.webapp.utils.slinkyUtils.facades.reactrouterdom.useHistory\nimport slinky.core.facade.Fragment\nimport slinky.core.facade.ReactElement.jsUndefOrToElement\nimport slinky.core.{FunctionalComponent, KeyAddingStage}\n\nimport scala.scalajs.js\nobject ForgotPasswordPage {\n  def apply(ctx: AppContext): KeyAddingStage =\n    component(Props(ctx = ctx))\n\n  case class Props(ctx: AppContext)\n\n  private val styling = new CSSPropertiesUtils {\n    maxWidth = 350\n    width = \"100%\"\n  }\n\n  val component: FunctionalComponent[Props] = FunctionalComponent[Props] { props =>\n    val texts = I18nHooks.useMessages(props.ctx.$lang)\n    val history = useHistory()\n\n    Container(\n      flex = Some(1),\n      justifyContent = Container.Alignment.center,\n      alignItems = Container.Alignment.center,\n      child = mui.Box.sx(styling)(\n        AppCard(\n          Fragment(\n            Container(\n              alignItems = Container.Alignment.center,\n              child = mui.Typography(texts.recoverYourPassword).variant(\"h5\")\n            ),\n            ForgotPasswordForm(props.ctx),\n            Container(\n              margin = Container.EdgeInsets.top(8),\n              flexDirection = FlexDirection.row,\n              alignItems = Container.Alignment.center,\n              justifyContent = Container.Alignment.center,\n              child = Fragment(\n                mui.Typography(texts.dontHaveAccountYet),\n                mui.Button\n                  .normal()(texts.signUp)\n                  .variant(\"text\")\n                  .color(\"primary\")\n                  .onClick(_ => history.push(\"/signUp\"))\n              )\n            )\n          )\n        )\n      )\n    )\n  }\n}\n"
  },
  {
    "path": "web/src/main/scala/net/wiringbits/components/pages/HomePage.scala",
    "content": "package net.wiringbits.components.pages\n\nimport com.olvind.mui.csstype.mod.Property.TextAlign\nimport com.olvind.mui.muiMaterial.components as mui\nimport net.wiringbits.AppContext\nimport net.wiringbits.core.I18nHooks\nimport net.wiringbits.webapp.utils.slinkyUtils.Utils.CSSPropertiesUtils\nimport net.wiringbits.webapp.utils.slinkyUtils.components.core.widgets.*\nimport slinky.core.facade.{Fragment, ReactElement}\nimport slinky.core.{FunctionalComponent, KeyAddingStage}\nimport slinky.web.html.*\n\nobject HomePage {\n  def apply(ctx: AppContext): KeyAddingStage =\n    component(Props(ctx = ctx))\n\n  case class Props(ctx: AppContext)\n\n  private val homeContainerStyling = new CSSPropertiesUtils {\n    maxWidth = 1300\n    width = \"100%\"\n  }\n\n  private val homeTitleStyling = new CSSPropertiesUtils {\n    textAlign = TextAlign.center\n    margin = \"8px 0\"\n\n  }\n\n  private val screenshotStyling = new CSSPropertiesUtils {\n    maxWidth = 1200\n    width = \"100%\"\n    display = \"block\"\n    margin = \"1em auto\"\n  }\n\n  val component: FunctionalComponent[Props] = FunctionalComponent[Props] { props =>\n    val texts = I18nHooks.useMessages(props.ctx.$lang)\n\n    def title(msg: String) = mui\n      .Typography(msg)\n      .variant(\"h4\")\n      .color(\"inherit\")\n\n    def paragraph(args: ReactElement) = mui\n      .Typography(args)\n      .variant(\"body1\")\n      .color(\"inherit\")\n\n    def link(msg: String, url: String) = mui\n      .Link(msg)\n      .href(url)\n      .target(\"_blank\")\n\n    def image(srcImg: String, altImg: String, classImg: String) =\n      img(src := srcImg, alt := altImg, className := classImg, style := screenshotStyling)\n\n    val homeFragment = Fragment(\n      mui.Typography\n        .sx(homeTitleStyling)(texts.homePage)\n        .variant(\"h4\")\n        .color(\"inherit\"),\n      paragraph(texts.homePageDescription),\n      br(),\n      br()\n    )\n\n    val userProfileFragment = Fragment(\n      title(texts.userProfile),\n      paragraph(\n        Fragment(\n          texts.userProfileDescription,\n          link(texts.tryIt.toLowerCase, \"https://template-demo.wiringbits.net/signin\")\n        )\n      ),\n      br(),\n      br()\n    )\n\n    val swaggerFragment = Fragment(\n      title(texts.swaggerIntegration),\n      paragraph(\n        Fragment(\n          texts.swaggerIntegrationDescription,\n          link(texts.tryIt.toLowerCase, \"https://template-demo.wiringbits.net/api/docs/index.html\")\n        )\n      ),\n      image(\"/img/home/swagger.png\", texts.swaggerIntegration, \"screenshot\"),\n      br(),\n      br()\n    )\n\n    val dataLoadingFragment = Fragment(\n      title(texts.consistentDataLoading),\n      paragraph(texts.consistentDataLoadingDescription),\n      image(\"/img/home/async-component-snippet.png\", texts.swaggerIntegration, \"snippet\"),\n      paragraph(texts.dataIsBeingLoaded),\n      image(\"/img/home/async-progress.png\", texts.swaggerIntegration, \"screenshot\"),\n      paragraph(texts.problemFetchingData),\n      image(\"/img/home/async-retry.png\", texts.swaggerIntegration, \"screenshot\"),\n      br(),\n      br()\n    )\n\n    val simpleArchitectureFragment = Fragment(\n      title(texts.simpleToFollowArchitecture),\n      paragraph(texts.simpleToFollowArchitectureDescription1),\n      paragraph(texts.simpleToFollowArchitectureDescription2),\n      br(),\n      br()\n    )\n\n    Container(\n      flex = Some(1),\n      alignItems = Container.Alignment.center,\n      child = mui.Box.sx(homeContainerStyling)(\n        homeFragment,\n        userProfileFragment,\n        swaggerFragment,\n        dataLoadingFragment,\n        simpleArchitectureFragment\n      )\n    )\n  }\n}\n"
  },
  {
    "path": "web/src/main/scala/net/wiringbits/components/pages/ResendVerifyEmailPage.scala",
    "content": "package net.wiringbits.components.pages\n\nimport net.wiringbits.AppContext\nimport net.wiringbits.components.widgets.ResendVerifyEmailForm\nimport net.wiringbits.webapp.utils.slinkyUtils.components.core.widgets.Container\nimport slinky.core.{FunctionalComponent, KeyAddingStage}\n\nobject ResendVerifyEmailPage {\n  def apply(ctx: AppContext): KeyAddingStage =\n    component(Props(ctx = ctx))\n\n  case class Props(ctx: AppContext)\n\n  val component: FunctionalComponent[Props] = FunctionalComponent[Props] { props =>\n    Container(\n      flex = Some(1),\n      justifyContent = Container.Alignment.center,\n      alignItems = Container.Alignment.center,\n      child = ResendVerifyEmailForm(props.ctx)\n    )\n  }\n}\n"
  },
  {
    "path": "web/src/main/scala/net/wiringbits/components/pages/ResetPasswordPage.scala",
    "content": "package net.wiringbits.components.pages\n\nimport com.olvind.mui.csstype.mod.Property.FlexDirection\nimport com.olvind.mui.muiMaterial.components as mui\nimport net.wiringbits.AppContext\nimport net.wiringbits.common.models.UserToken\nimport net.wiringbits.components.widgets.{AppCard, ResetPasswordForm}\nimport net.wiringbits.core.I18nHooks\nimport net.wiringbits.webapp.utils.slinkyUtils.Utils.CSSPropertiesUtils\nimport net.wiringbits.webapp.utils.slinkyUtils.components.core.widgets.Container\nimport net.wiringbits.webapp.utils.slinkyUtils.facades.reactrouterdom.{useHistory, useParams}\nimport slinky.core.facade.Fragment\nimport slinky.core.{FunctionalComponent, KeyAddingStage}\n\nimport scala.scalajs.js\n\nobject ResetPasswordPage {\n  def apply(ctx: AppContext): KeyAddingStage =\n    component(Props(ctx = ctx))\n\n  case class Props(ctx: AppContext)\n\n  private val styling = new CSSPropertiesUtils {\n    maxWidth = 350\n    width = \"100%\"\n  }\n\n  val component: FunctionalComponent[Props] = FunctionalComponent[Props] { props =>\n    val texts = I18nHooks.useMessages(props.ctx.$lang)\n    val history = useHistory()\n    val params = useParams()\n    val resetPasswordCode = params.get(\"resetPasswordCode\").getOrElse(\"\")\n    val userToken = UserToken.validate(resetPasswordCode)\n\n    Container(\n      flex = Some(1),\n      justifyContent = Container.Alignment.center,\n      alignItems = Container.Alignment.center,\n      child = mui.Box.sx(styling)(\n        AppCard(\n          Fragment(\n            Container(\n              alignItems = Container.Alignment.center,\n              justifyContent = Container.Alignment.center,\n              child = mui.Typography(texts.enterNewPassword).variant(\"h5\")\n            ),\n            ResetPasswordForm(props.ctx, userToken),\n            Container(\n              margin = Container.EdgeInsets.top(8),\n              flexDirection = FlexDirection.row,\n              alignItems = Container.Alignment.center,\n              justifyContent = Container.Alignment.center,\n              child = Fragment(\n                mui.Typography(texts.alreadyHaveAccount),\n                mui.Button\n                  .normal(texts.signIn)\n                  .variant(\"text\")\n                  .color(\"primary\")\n                  .onClick(_ => history.push(\"/signin\"))\n              )\n            )\n          )\n        )\n      )\n    )\n  }\n}\n"
  },
  {
    "path": "web/src/main/scala/net/wiringbits/components/pages/SignInPage.scala",
    "content": "package net.wiringbits.components.pages\n\nimport com.olvind.mui.csstype.mod.Property.FlexDirection\nimport com.olvind.mui.muiMaterial.components as mui\nimport net.wiringbits.AppContext\nimport net.wiringbits.components.widgets.*\nimport net.wiringbits.core.I18nHooks\nimport net.wiringbits.webapp.utils.slinkyUtils.Utils.CSSPropertiesUtils\nimport net.wiringbits.webapp.utils.slinkyUtils.components.core.widgets.{Container, Title}\nimport net.wiringbits.webapp.utils.slinkyUtils.facades.reactrouterdom.useHistory\nimport slinky.core.facade.Fragment\nimport slinky.core.{FunctionalComponent, KeyAddingStage}\n\nimport scala.scalajs.js\n\nobject SignInPage {\n  def apply(ctx: AppContext): KeyAddingStage =\n    component(Props(ctx = ctx))\n\n  case class Props(ctx: AppContext)\n\n  private val styling = new CSSPropertiesUtils {\n    maxWidth = 350\n    width = \"100%\"\n  }\n\n  val component: FunctionalComponent[Props] = FunctionalComponent[Props] { props =>\n    val texts = I18nHooks.useMessages(props.ctx.$lang)\n    val history = useHistory()\n\n    Container(\n      flex = Some(1),\n      justifyContent = Container.Alignment.center,\n      alignItems = Container.Alignment.center,\n      child = mui.Box.sx(styling)(\n        AppCard(\n          Fragment(\n            Container(\n              justifyContent = Container.Alignment.center,\n              alignItems = Container.Alignment.center,\n              child = Title(texts.signIn)\n            ),\n            Container(\n              flex = Some(1),\n              alignItems = Container.Alignment.center,\n              justifyContent = Container.Alignment.center,\n              padding = Container.EdgeInsets.top(16),\n              child = SignInForm(props.ctx)\n            ),\n            Container(\n              margin = Container.EdgeInsets.top(8),\n              flexDirection = FlexDirection.row,\n              alignItems = Container.Alignment.center,\n              justifyContent = Container.Alignment.center,\n              child = Fragment(\n                mui.Typography(texts.dontHaveAccountYet),\n                mui.Button\n                  .normal()(texts.signUp)\n                  .variant(\"text\")\n                  .color(\"primary\")\n                  .onClick(_ => history.push(\"/signUp\"))\n              )\n            ),\n            Container(\n              flexDirection = FlexDirection.row,\n              alignItems = Container.Alignment.center,\n              justifyContent = Container.Alignment.center,\n              child = Fragment(\n                mui.Typography(texts.forgotYourPassword),\n                mui.Button\n                  .normal(texts.recoverIt)\n                  .variant(\"text\")\n                  .color(\"primary\")\n                  .onClick(_ => history.push(\"/forgot-password\"))\n              )\n            )\n          )\n        )\n      )\n    )\n  }\n}\n"
  },
  {
    "path": "web/src/main/scala/net/wiringbits/components/pages/SignUpPage.scala",
    "content": "package net.wiringbits.components.pages\n\nimport net.wiringbits.AppContext\nimport net.wiringbits.components.widgets.SignUpForm\nimport net.wiringbits.webapp.utils.slinkyUtils.components.core.widgets.Container\nimport slinky.core.{FunctionalComponent, KeyAddingStage}\n\nobject SignUpPage {\n  def apply(ctx: AppContext): KeyAddingStage =\n    component(Props(ctx = ctx))\n\n  case class Props(ctx: AppContext)\n\n  val component: FunctionalComponent[Props] = FunctionalComponent[Props] { props =>\n    Container(\n      flex = Some(1),\n      alignItems = Container.Alignment.center,\n      justifyContent = Container.Alignment.center,\n      child = SignUpForm(props.ctx)\n    )\n  }\n}\n"
  },
  {
    "path": "web/src/main/scala/net/wiringbits/components/pages/UserEditPage.scala",
    "content": "package net.wiringbits.components.pages\n\nimport com.olvind.mui.muiMaterial.components as mui\nimport net.wiringbits.AppContext\nimport net.wiringbits.components.widgets.{EditPasswordForm, UserInfo}\nimport net.wiringbits.core.I18nHooks\nimport net.wiringbits.models.UserMenuOption.{EditPassword, EditSummary}\nimport net.wiringbits.models.{User, UserMenuOption}\nimport net.wiringbits.webapp.utils.slinkyUtils.components.core.widgets.{Container, Title}\nimport slinky.core.facade.{Fragment, Hooks}\nimport slinky.core.{FunctionalComponent, KeyAddingStage}\n\nobject UserEditPage {\n  def apply(ctx: AppContext, user: User): KeyAddingStage =\n    component(Props(ctx = ctx, user = user))\n\n  case class Props(ctx: AppContext, user: User)\n\n  val component: FunctionalComponent[Props] = FunctionalComponent[Props] { props =>\n    val texts = I18nHooks.useMessages(props.ctx.$lang)\n    val (menuOption, setMenuOption) = Hooks.useState[UserMenuOption](UserMenuOption.EditSummary)\n\n    val header = Container(\n      margin = Container.EdgeInsets.bottom(16),\n      child = Title(texts.user)\n    )\n\n    val tabs = mui.CardContent()(\n      mui\n        .Tabs()(\n          UserMenuOption.values.map(x => mui.Tab.normal().label(texts.userMenuOption(x)).withKey(x.toString).build)\n        )\n        .value(UserMenuOption.values.indexOf(menuOption))\n        .onChange((_, index) => setMenuOption(UserMenuOption.values(index.toString.toInt)))\n    )\n\n    val body = mui.CardContent()(\n      menuOption match {\n        case EditSummary => UserInfo(props.ctx, props.user)\n        case EditPassword => EditPasswordForm(props.ctx, props.user)\n      }\n    )\n\n    Fragment(\n      header,\n      mui.Paper()(\n        mui.Card()(\n          tabs,\n          body\n        )\n      )\n    )\n  }\n\n}\n"
  },
  {
    "path": "web/src/main/scala/net/wiringbits/components/pages/VerifyEmailPage.scala",
    "content": "package net.wiringbits.components.pages\n\nimport com.olvind.mui.csstype.mod.Property.{FlexDirection, TextAlign}\nimport com.olvind.mui.muiMaterial.components as mui\nimport net.wiringbits.AppContext\nimport net.wiringbits.core.I18nHooks\nimport net.wiringbits.webapp.utils.slinkyUtils.Utils.CSSPropertiesUtils\nimport net.wiringbits.webapp.utils.slinkyUtils.facades.reactrouterdom.{useHistory, useLocation}\nimport org.scalajs.dom\nimport org.scalajs.dom.URLSearchParams\nimport slinky.core.facade.Fragment\nimport slinky.core.{FunctionalComponent, KeyAddingStage}\nimport slinky.web.html.br\n\nimport scala.scalajs.js\n\nobject VerifyEmailPage {\n  def apply(ctx: AppContext): KeyAddingStage =\n    component(Props(ctx = ctx))\n\n  case class Props(ctx: AppContext)\n\n  private val emailPageStyling = new CSSPropertiesUtils {\n    flex = 1\n    display = \"flex\"\n    flexDirection = FlexDirection.column\n    alignItems = \"center\"\n    textAlign = TextAlign.center\n    justifyContent = \"center\"\n  }\n\n  private val emailTitleStyling = new CSSPropertiesUtils {\n    fontWeight = 600\n  }\n\n  val component: FunctionalComponent[Props] = FunctionalComponent[Props] { props =>\n    val texts = I18nHooks.useMessages(props.ctx.$lang)\n    val history = useHistory()\n    val params = new URLSearchParams(useLocation().asInstanceOf[js.Dynamic].search.asInstanceOf[String])\n    val emailParam = Option(params.get(\"email\")).getOrElse(\"\")\n\n    Fragment(\n      mui.Box.sx(emailPageStyling)(\n        mui\n          .Typography(texts.verifyYourEmailAddress)\n          .variant(\"h5\")\n          .className(\"emailTitle\")\n          .sx(emailTitleStyling),\n        br(),\n        mui\n          .Typography(\n            texts.emailHasBeenSent\n          )\n          .variant(\"h6\"),\n        mui\n          .Typography(\n            texts.emailNotReceived\n          )\n          .variant(\"h6\"),\n        br(),\n        mui.Button\n          .normal()(texts.resendEmail)\n          .variant(\"contained\")\n          .color(\"primary\")\n          .onClick(_ => history.push(s\"/resend-verify-email?email=$emailParam\"))\n      )\n    )\n  }\n\n}\n"
  },
  {
    "path": "web/src/main/scala/net/wiringbits/components/pages/VerifyEmailWithTokenPage.scala",
    "content": "package net.wiringbits.components.pages\n\nimport com.olvind.mui.csstype.mod.Property.{FlexDirection, TextAlign}\nimport com.olvind.mui.muiMaterial.components as mui\nimport net.wiringbits.AppContext\nimport net.wiringbits.api.models.users.VerifyEmail\nimport net.wiringbits.common.models.UserToken\nimport net.wiringbits.core.I18nHooks\nimport net.wiringbits.webapp.utils.slinkyUtils.Utils.CSSPropertiesUtils\nimport net.wiringbits.webapp.utils.slinkyUtils.components.core.widgets.{CircularLoader, Container}\nimport net.wiringbits.webapp.utils.slinkyUtils.facades.reactrouterdom.{useHistory, useParams}\nimport org.scalajs.macrotaskexecutor.MacrotaskExecutor.Implicits.global\nimport slinky.core.facade.{Fragment, Hooks}\nimport slinky.core.{FunctionalComponent, KeyAddingStage}\n\nimport scala.scalajs.js\nimport scala.scalajs.js.timers.setTimeout\nimport scala.util.{Failure, Success}\n\nobject VerifyEmailWithTokenPage {\n  def apply(ctx: AppContext): KeyAddingStage =\n    component(Props(ctx = ctx))\n\n  case class Props(ctx: AppContext)\n\n  private case class State(\n      loading: Boolean,\n      error: Option[String],\n      title: String,\n      message: String\n  )\n\n  private val emailPageStyling = new CSSPropertiesUtils {\n    flex = 1\n    display = \"flex\"\n    flexDirection = FlexDirection.column\n    alignItems = \"center\"\n    textAlign = TextAlign.center\n    justifyContent = \"center\"\n  }\n\n  private val emailTitleStyling = new CSSPropertiesUtils {\n    fontWeight = 600\n  }\n\n  val component: FunctionalComponent[Props] = FunctionalComponent[Props] { props =>\n    val texts = I18nHooks.useMessages(props.ctx.$lang)\n\n    val initialState = State(\n      loading = false,\n      error = None,\n      title = texts.verifyingEmail,\n      message = texts.waitAMomentPlease\n    )\n\n    val history = useHistory()\n    val params = useParams()\n    val (state, setState) = Hooks.useState(initialState)\n    val emailCodeOpt = UserToken.validate(params.get(\"emailCode\").getOrElse(\"\"))\n\n    def sendEmailCode(): Unit = {\n      setState(_.copy(loading = true))\n      emailCodeOpt match {\n        case Some(emailCode) =>\n          props.ctx.api.client.verifyEmail(VerifyEmail.Request(emailCode)).onComplete {\n            case Success(_) =>\n              val title = texts.successfulEmailVerification\n              val message = texts.goingToBeRedirected\n              setState(_.copy(loading = false, title = title, message = message))\n              setTimeout(2000) {\n                history.push(\"/signin\")\n              }\n\n            case Failure(ex) =>\n              val title = texts.failedEmailVerification\n              val message = ex.getMessage\n              setState(_.copy(loading = false, title = title, message = message, error = Some(message)))\n          }\n        case None =>\n          val title = texts.failedEmailVerification\n          val message = texts.invalidVerificationToken\n          setState(_.copy(loading = false, title = title, message = message, error = Some(message)))\n      }\n    }\n\n    Hooks.useEffect(() => sendEmailCode(), \"\")\n\n    val loading =\n      if (state.loading || state.error.isEmpty)\n        Fragment(\n          loader\n        )\n      else {\n        Fragment(\n        )\n      }\n\n    mui.Box.sx(emailPageStyling)(\n      mui.Typography(state.title).variant(\"h5\").className(\"emailTitle\").sx(emailTitleStyling),\n      mui.Typography(state.message).variant(\"h6\"),\n      loading\n    )\n  }\n\n  private def loader = Container(\n    alignItems = Container.Alignment.center,\n    justifyContent = Container.Alignment.center,\n    padding = Container.EdgeInsets.vertical(16),\n    child = CircularLoader(50)\n  )\n}\n"
  },
  {
    "path": "web/src/main/scala/net/wiringbits/components/widgets/AppBar.scala",
    "content": "package net.wiringbits.components.widgets\n\nimport com.olvind.mui.muiIconsMaterial.components as muiIcons\nimport com.olvind.mui.muiMaterial.components as mui\nimport com.olvind.mui.muiMaterial.mod.PropTypes.Color\nimport net.wiringbits.AppContext\nimport net.wiringbits.core.{I18nHooks, ReactiveHooks}\nimport net.wiringbits.models.AuthState\nimport net.wiringbits.webapp.utils.slinkyUtils.Utils.CSSPropertiesUtils\nimport net.wiringbits.webapp.utils.slinkyUtils.components.core.widgets.{Container, NavLinkButton, Subtitle, Title}\nimport net.wiringbits.webapp.utils.slinkyUtils.core.MediaQueryHooks\nimport slinky.core.facade.{Fragment, Hooks, ReactElement}\nimport slinky.core.{FunctionalComponent, KeyAddingStage}\n\nobject AppBar {\n  def apply(ctx: AppContext): KeyAddingStage =\n    component(Props(ctx))\n\n  case class Props(ctx: AppContext)\n\n  private val appbarStyling = new CSSPropertiesUtils {\n    color = \"#FFF\"\n  }\n\n  private val toolBarStyling = new CSSPropertiesUtils {\n    display = \"flex\"\n    alignItems = \"center\"\n    justifyContent = \"space-between\"\n  }\n\n  private val toolbarMobileStyling = new CSSPropertiesUtils {\n    display = \"flex\"\n    alignItems = \"center\"\n  }\n\n  private val menuStyling = new CSSPropertiesUtils {\n    display = \"flex\"\n  }\n\n  val component: FunctionalComponent[Props] = FunctionalComponent[Props] { props =>\n    val texts = I18nHooks.useMessages(props.ctx.$lang)\n    val auth = ReactiveHooks.useDistinctValue(props.ctx.$auth)\n    val isMobileOrTablet = MediaQueryHooks.useIsMobileOrTablet()\n    val (visibleDrawer, setVisibleDrawer) = Hooks.useState(false)\n\n    def onButtonClick(): Unit = {\n      if (visibleDrawer) {\n        setVisibleDrawer(false)\n      }\n    }\n\n    val menu = auth match {\n      case AuthState.Authenticated(_) =>\n        Fragment(\n          NavLinkButton(\"/\", texts.home, onButtonClick),\n          NavLinkButton(\"/dashboard\", texts.dashboard, onButtonClick),\n          NavLinkButton(\"/about\", texts.about, onButtonClick),\n          NavLinkButton(\"/me\", texts.profile, onButtonClick),\n          NavLinkButton(\"/signout\", texts.signOut, onButtonClick)\n        )\n\n      case AuthState.Unauthenticated =>\n        Fragment(\n          NavLinkButton(\"/\", texts.home, onButtonClick),\n          NavLinkButton(\"/about\", texts.about, onButtonClick),\n          NavLinkButton(\"/signup\", texts.signUp, onButtonClick),\n          NavLinkButton(\"/signin\", texts.signIn, onButtonClick)\n        )\n    }\n\n    if (isMobileOrTablet) {\n      val drawerContent = Container(\n        minWidth = Some(\"256px\"),\n        flex = Some(1),\n        margin = Container.EdgeInsets.bottom(32),\n        alignItems = Container.Alignment.flexEnd,\n        justifyContent = Container.Alignment.spaceBetween,\n        child = Fragment(\n          mui.AppBar\n            .sx(appbarStyling)\n            .position(\"relative\")(\n              mui.Toolbar\n                .sx(toolbarMobileStyling)(\n                  Subtitle(texts.appName)\n                )\n            ),\n          Container(\n            alignItems = Container.Alignment.flexEnd,\n            justifyContent = Container.Alignment.spaceBetween,\n            child = menu\n          )\n        )\n      )\n\n      val drawer = mui\n        .SwipeableDrawer(\n          onOpen = _ => setVisibleDrawer(true),\n          onClose = _ => setVisibleDrawer(false)\n        )(drawerContent)\n        .open(visibleDrawer)\n\n      val toolbar = mui.Toolbar.sx(toolbarMobileStyling)(\n        mui.IconButton\n          .normal()(mui.Icon(muiIcons.Menu()))\n          .color(Color.inherit)\n          .onClick(_ => setVisibleDrawer(true)),\n        Subtitle(texts.appName)\n      )\n\n      mui.AppBar\n        .sx(appbarStyling)\n        .position(\"relative\")(toolbar, drawer)\n    } else {\n      mui.AppBar\n        .sx(appbarStyling)\n        .position(\"relative\")(\n          mui.Toolbar.sx(toolBarStyling)(\n            Title(texts.appName),\n            mui.Box.sx(menuStyling)(menu)\n          )\n        )\n    }\n  }\n}\n"
  },
  {
    "path": "web/src/main/scala/net/wiringbits/components/widgets/AppCard.scala",
    "content": "package net.wiringbits.components.widgets\n\nimport com.olvind.mui.csstype.mod.Property.FlexDirection\nimport com.olvind.mui.muiMaterial.components as mui\nimport net.wiringbits.webapp.utils.slinkyUtils.Utils.CSSPropertiesUtils\nimport slinky.core.facade.{Fragment, ReactElement}\nimport slinky.core.{FunctionalComponent, KeyAddingStage}\n\nimport scala.scalajs.js\n\nobject AppCard {\n  def apply(child: ReactElement, title: Option[String] = None, centerTitle: Boolean = false): KeyAddingStage =\n    component(Props(child = child, title = title, centerTitle = centerTitle))\n\n  case class Props(child: ReactElement, title: Option[String] = None, centerTitle: Boolean = false)\n\n  private val appCardStyling = new CSSPropertiesUtils {\n    width = \"100%\"\n    display = \"flex\"\n    flexDirection = FlexDirection.column\n    border = \"1px solid rgba(0, 0, 0, 0.12)\"\n    overflow = \"hidden\"\n  }\n  private val appCardHeadStyling = new CSSPropertiesUtils {\n    padding = \"16px 16px 0 16px\"\n  }\n\n  private val appCardBodyStyling = new CSSPropertiesUtils {\n    padding = \"25px 16px\"\n  }\n\n  val component: FunctionalComponent[Props] = FunctionalComponent[Props] { props =>\n    val head: ReactElement = props.title match {\n      case Some(title) =>\n        val textStyle = new CSSPropertiesUtils {\n          textAlign = if (props.centerTitle) \"center\" else \"left\"\n          fontWeight = 700\n        }\n\n        mui.Box.sx(appCardHeadStyling)(\n          mui\n            .Typography(title)\n            .sx(textStyle)\n            .variant(\"h5\")\n            .color(\"inherit\")\n        )\n      case None => Fragment()\n    }\n    val body = mui.Box.sx(appCardBodyStyling)(props.child)\n\n    mui.Paper\n      .sx(appCardStyling)\n      .elevation(0)(head, body)\n  }\n}\n"
  },
  {
    "path": "web/src/main/scala/net/wiringbits/components/widgets/EditPasswordForm.scala",
    "content": "package net.wiringbits.components.widgets\n\nimport com.olvind.mui.muiMaterial.components as mui\nimport net.wiringbits.AppContext\nimport net.wiringbits.core.I18nHooks\nimport net.wiringbits.forms.UpdatePasswordFormData\nimport net.wiringbits.models.User\nimport net.wiringbits.ui.components.inputs.PasswordInput\nimport net.wiringbits.webapp.utils.slinkyUtils.components.core.ErrorLabel\nimport net.wiringbits.webapp.utils.slinkyUtils.components.core.widgets.{CircularLoader, Container}\nimport net.wiringbits.webapp.utils.slinkyUtils.forms.StatefulFormData\nimport org.scalajs.dom\nimport org.scalajs.macrotaskexecutor.MacrotaskExecutor.Implicits.global\nimport slinky.core.facade.{Fragment, Hooks}\nimport slinky.core.{FunctionalComponent, KeyAddingStage, SyntheticEvent}\nimport slinky.web.html.{form, onSubmit}\n\nimport scala.util.{Failure, Success}\n\nobject EditPasswordForm {\n  def apply(ctx: AppContext, user: User): KeyAddingStage =\n    component(Props(ctx = ctx, user = user))\n\n  case class Props(ctx: AppContext, user: User)\n\n  val component: FunctionalComponent[Props] = FunctionalComponent[Props] { props =>\n    val texts = I18nHooks.useMessages(props.ctx.$lang)\n    val (formData, setFormData) = Hooks.useState(\n      StatefulFormData(\n        UpdatePasswordFormData.initial(\n          oldPasswordLabel = texts.oldPassword,\n          passwordLabel = texts.password,\n          repeatPasswordLabel = texts.repeatPassword\n        )\n      )\n    )\n\n    def onDataChanged(f: UpdatePasswordFormData => UpdatePasswordFormData): Unit = {\n      setFormData { current =>\n        current.filling.copy(data = f(current.data))\n      }\n    }\n\n    def handleSubmit(e: SyntheticEvent[_, dom.Event]): Unit = {\n      e.preventDefault()\n\n      if (formData.isSubmitButtonEnabled) {\n        setFormData(_.submit)\n        for {\n          request <- formData.data.submitRequest\n            .orElse {\n              setFormData(_.submissionFailed(texts.completeData))\n              None\n            }\n        } yield props.ctx.api.client\n          .updatePassword(request)\n          .onComplete {\n            case Success(_) =>\n              // TODO: Show dialog?\n              setFormData(_.submitted)\n\n            case Failure(ex) =>\n              setFormData(_.submissionFailed(ex.getMessage))\n          }\n      } else {\n        println(\"Submit fired when it is not available\")\n      }\n    }\n\n    val oldPasswordInput = PasswordInput\n      .component(\n        PasswordInput.Props(\n          formData.data.oldPassword,\n          disabled = formData.isInputDisabled,\n          onChange = value => onDataChanged(x => x.copy(oldPassword = x.oldPassword.updated(value)))\n        )\n      )\n\n    val passwordInput = PasswordInput\n      .component(\n        PasswordInput.Props(\n          formData.data.password,\n          disabled = formData.isInputDisabled,\n          onChange = value => onDataChanged(x => x.copy(password = x.password.updated(value)))\n        )\n      )\n\n    val repeatPasswordInput = PasswordInput\n      .component(\n        PasswordInput.Props(\n          formData.data.repeatPassword,\n          disabled = formData.isInputDisabled,\n          onChange = value => onDataChanged(x => x.copy(repeatPassword = x.repeatPassword.updated(value)))\n        )\n      )\n\n    val saveButton = {\n      val text = if (formData.isSubmitting) {\n        Fragment(\n          CircularLoader(),\n          Container(margin = Container.EdgeInsets.left(8), child = texts.loading)\n        )\n      } else {\n        Fragment(texts.savePassword)\n      }\n\n      mui.Button\n        .normal()(text)\n        .fullWidth(true)\n        .disabled(formData.isSubmitButtonDisabled)\n        .variant(\"contained\")\n        .color(\"primary\")\n        .size(\"large\")\n        .`type`(\"submit\")\n    }\n\n    val error =\n      formData.firstValidationError.map { text =>\n        Container(\n          margin = Container.EdgeInsets.vertical(16),\n          child = ErrorLabel(text)\n        )\n      }\n\n    form(onSubmit := (handleSubmit(_)))(\n      oldPasswordInput,\n      passwordInput,\n      repeatPasswordInput,\n      Container(\n        alignItems = Container.Alignment.center,\n        justifyContent = Container.Alignment.center,\n        child = error\n      ),\n      saveButton\n    )\n  }\n}\n"
  },
  {
    "path": "web/src/main/scala/net/wiringbits/components/widgets/EditUserForm.scala",
    "content": "package net.wiringbits.components.widgets\n\nimport com.olvind.mui.muiMaterial.components as mui\nimport net.wiringbits.AppContext\nimport net.wiringbits.api.models.auth.GetCurrentUser\nimport net.wiringbits.api.utils.Formatter\nimport net.wiringbits.core.I18nHooks\nimport net.wiringbits.forms.UpdateInfoFormData\nimport net.wiringbits.models.User\nimport net.wiringbits.ui.components.inputs.{EmailInput, NameInput}\nimport net.wiringbits.webapp.utils.slinkyUtils.components.core.ErrorLabel\nimport net.wiringbits.webapp.utils.slinkyUtils.components.core.widgets.{CircularLoader, Container}\nimport net.wiringbits.webapp.utils.slinkyUtils.forms.StatefulFormData\nimport org.scalajs.dom\nimport org.scalajs.macrotaskexecutor.MacrotaskExecutor.Implicits.global\nimport slinky.core.facade.{Fragment, Hooks}\nimport slinky.core.{FunctionalComponent, KeyAddingStage, SyntheticEvent}\nimport slinky.web.html.{form, onSubmit}\n\nimport scala.util.{Failure, Success}\n\nobject EditUserForm {\n  def apply(ctx: AppContext, user: User, response: GetCurrentUser.Response, onSave: () => Unit): KeyAddingStage =\n    component(Props(ctx = ctx, user = user, response = response, onSave = onSave))\n\n  case class Props(ctx: AppContext, user: User, response: GetCurrentUser.Response, onSave: () => Unit)\n\n  val component: FunctionalComponent[Props] = FunctionalComponent[Props] { props =>\n    val texts = I18nHooks.useMessages(props.ctx.$lang)\n    val (hasChanges, setHasChanges) = Hooks.useState(false)\n    val (formData, setFormData) = Hooks.useState(\n      StatefulFormData(\n        UpdateInfoFormData.initial(\n          nameLabel = texts.name,\n          nameInitialValue = Some(props.response.name),\n          emailLabel = texts.email,\n          emailValue = Some(props.response.email)\n        )\n      )\n    )\n\n    def onDataChanged(f: UpdateInfoFormData => UpdateInfoFormData): Unit = {\n      setFormData { current =>\n        current.filling.copy(data = f(current.data))\n      }\n    }\n\n    def handleSubmit(e: SyntheticEvent[_, dom.Event]): Unit = {\n      e.preventDefault()\n\n      if (formData.isSubmitButtonEnabled) {\n        setFormData(_.submit)\n        for {\n          request <- formData.data.submitRequest\n            .orElse {\n              setFormData(_.submissionFailed(texts.completeData))\n              None\n            }\n        } yield props.ctx.api.client\n          .updateUser(request)\n          .onComplete {\n            case Success(_) =>\n              setFormData(_.submitted)\n              props.onSave()\n\n            case Failure(ex) =>\n              setFormData(_.submissionFailed(ex.getMessage))\n          }\n      } else {\n        println(\"Submit fired when it is not available\")\n      }\n    }\n\n    val nameInput = NameInput\n      .component(\n        NameInput.Props(\n          formData.data.name,\n          disabled = formData.isInputDisabled,\n          onChange = value => {\n            setHasChanges(value.input != props.response.name.string)\n            onDataChanged(x => x.copy(name = x.name.updated(value)))\n          }\n        )\n      )\n\n    val emailInput = EmailInput\n      .component(\n        EmailInput.Props(\n          formData.data.email,\n          disabled = true,\n          onChange = _ => ()\n        )\n      )\n\n    val saveButton = {\n      val text = if (formData.isSubmitting) {\n        Fragment(\n          CircularLoader(),\n          Container(margin = Container.EdgeInsets.left(8), child = texts.loading)\n        )\n      } else {\n        Fragment(texts.save)\n      }\n\n      mui.Button\n        .normal()(text)\n        .fullWidth(true)\n        .disabled(formData.isSubmitButtonDisabled || !hasChanges)\n        .variant(\"contained\")\n        .color(\"primary\")\n        .size(\"large\")\n        .`type`(\"submit\")\n    }\n\n    val createdAt =\n      Fragment(\n        mui.Typography(texts.createdAt).variant(\"subtitle2\"),\n        mui.Typography(Formatter.instant(props.response.createdAt))\n      )\n\n    form(onSubmit := (handleSubmit(_)))(\n      nameInput,\n      emailInput,\n      formData.firstValidationError.map { text =>\n        Container(\n          margin = Container.EdgeInsets.top(16),\n          child = ErrorLabel(text)\n        )\n      },\n      createdAt,\n      saveButton\n    )\n  }\n}\n"
  },
  {
    "path": "web/src/main/scala/net/wiringbits/components/widgets/Footer.scala",
    "content": "package net.wiringbits.components.widgets\n\nimport com.olvind.mui.csstype.mod.Property.FlexDirection\nimport com.olvind.mui.muiMaterial.components as mui\nimport com.olvind.mui.muiMaterial.mod.PropTypes.Color\nimport net.wiringbits.AppContext\nimport net.wiringbits.core.I18nHooks\nimport net.wiringbits.webapp.utils.slinkyUtils.Utils.CSSPropertiesUtils\nimport net.wiringbits.webapp.utils.slinkyUtils.components.core.widgets.Container\nimport net.wiringbits.webapp.utils.slinkyUtils.core.MediaQueryHooks\nimport slinky.core.facade.Fragment\nimport slinky.core.{FunctionalComponent, KeyAddingStage}\n\nobject Footer {\n  def apply(ctx: AppContext): KeyAddingStage =\n    component(Props(ctx = ctx))\n\n  case class Props(ctx: AppContext)\n\n  private val styling = new CSSPropertiesUtils {\n    color = \"#FFF\"\n    backgroundColor = \"#222\"\n    borderRadius = 0\n  }\n\n  private val margin = 16\n\n  val component: FunctionalComponent[Props] = FunctionalComponent[Props] { props =>\n    val texts = I18nHooks.useMessages(props.ctx.$lang)\n    val isMobileOrTablet = MediaQueryHooks.useIsMobileOrTablet()\n\n    val appName = Container(\n      margin = Container.EdgeInsets.bottom(margin),\n      child = mui.Typography(texts.appName).variant(\"h4\").color(Color.inherit)\n    )\n    val appDescription =\n      mui.Typography(texts.description).variant(\"body2\").color(Color.inherit)\n\n    def title(text: String) =\n      Container(\n        margin = Container.EdgeInsets.bottom(margin),\n        child = mui.Typography(text).variant(\"h5\").color(Color.inherit)\n      )\n\n    def subtitle(text: String) =\n      mui.Typography(text).variant(\"subtitle2\").color(Color.inherit)\n\n    def link(text: String, url: String) =\n      mui\n        .Link(\n          mui.Typography(text).variant(\"body2\").color(Color.inherit)\n        )\n        .href(url)\n        .color(Color.inherit)\n\n    val copyright = Container(\n      margin = Container.EdgeInsets.vertical(margin),\n      alignItems = Container.Alignment.center,\n      child = mui.Typography(texts.appNameCopyright).color(Color.inherit)\n    )\n\n    val projects = Container(\n      flex = Some(1),\n      child = Fragment(\n        title(\"Projects\"),\n        link(\"CollabUML\", \"https://collabuml.com\"),\n        link(\"The Stakenet Block Explorer\", \"https://xsnexplorer.io/\"),\n        link(\"The Stakenet Orderbook\", \"https://orderbook.stakenet.io/XSN_BTC\"),\n        link(\"Pull Request Attention\", \"https://prattention.com\"),\n        link(\"CazaDescuentos\", \"https://cazadescuentos.net\"),\n        link(\"safer.chat\", \"https://safer.chat\"),\n        link(\"Crypto Coin Alerts\", \"https://github.com/AlexITC/crypto-coin-alerts\")\n      )\n    )\n\n    val contact = Container(\n      flex = Some(1),\n      child = Fragment(\n        title(texts.contact),\n        Container(\n          child = Fragment(\n            subtitle(texts.contact),\n            link(props.ctx.contactEmail.string, s\"mailto:${props.ctx.contactEmail.string}\")\n          )\n        ),\n        Container(\n          margin = Container.EdgeInsets.top(margin / 2),\n          child = Fragment(\n            subtitle(texts.phone),\n            mui.Typography(props.ctx.contactPhone).variant(\"body2\").color(Color.inherit)\n          )\n        )\n      )\n    )\n\n    val body = if (isMobileOrTablet) {\n      Container(\n        padding = Container.EdgeInsets.all(margin),\n        child = Fragment(\n          appName,\n          appDescription,\n          Container(margin = Container.EdgeInsets.top(margin), child = projects),\n          Container(margin = Container.EdgeInsets.top(margin), child = contact)\n        )\n      )\n    } else {\n\n      Container(\n        padding = Container.EdgeInsets.all(margin),\n        flexDirection = FlexDirection.row,\n        child = Fragment(\n          Container(\n            margin = Container.EdgeInsets.right(margin / 2),\n            flex = Some(1),\n            child = Fragment(appName, appDescription)\n          ),\n          Container(\n            flex = Some(1),\n            margin = Container.EdgeInsets.left(margin / 2),\n            flexDirection = FlexDirection.row,\n            child = Fragment(\n              projects,\n              contact\n            )\n          )\n        )\n      )\n    }\n\n    mui\n      .Paper()(\n        Fragment(\n          body,\n          copyright\n        )\n      )\n      .sx(styling)\n  }\n}\n"
  },
  {
    "path": "web/src/main/scala/net/wiringbits/components/widgets/ForgotPasswordForm.scala",
    "content": "package net.wiringbits.components.widgets\n\nimport com.olvind.mui.muiMaterial.components as mui\nimport net.wiringbits.AppContext\nimport net.wiringbits.core.I18nHooks\nimport net.wiringbits.forms.ForgotPasswordFormData\nimport net.wiringbits.ui.components.inputs.EmailInput\nimport net.wiringbits.webapp.utils.slinkyUtils.components.core.ErrorLabel\nimport net.wiringbits.webapp.utils.slinkyUtils.components.core.widgets.{CircularLoader, Container}\nimport net.wiringbits.webapp.utils.slinkyUtils.facades.reactrouterdom.useHistory\nimport net.wiringbits.webapp.utils.slinkyUtils.forms.StatefulFormData\nimport org.scalajs.dom\nimport org.scalajs.macrotaskexecutor.MacrotaskExecutor.Implicits.global\nimport slinky.core.facade.{Fragment, Hooks}\nimport slinky.core.{FunctionalComponent, KeyAddingStage, SyntheticEvent}\nimport slinky.web.html.{form, onSubmit}\n\nimport scala.scalajs.js\nimport scala.util.{Failure, Success}\n\nobject ForgotPasswordForm {\n  def apply(ctx: AppContext): KeyAddingStage =\n    component(Props(ctx = ctx))\n\n  case class Props(ctx: AppContext)\n\n  val component: FunctionalComponent[Props] = FunctionalComponent[Props] { props =>\n    val texts = I18nHooks.useMessages(props.ctx.$lang)\n    val history = useHistory()\n\n    val (formData, setFormData) = Hooks.useState(\n      StatefulFormData(\n        ForgotPasswordFormData.initial(\n          emailLabel = texts.email\n        )\n      )\n    )\n\n    def onDataChanged(f: ForgotPasswordFormData => ForgotPasswordFormData): Unit = {\n      setFormData { current =>\n        current.filling.copy(data = f(current.data))\n      }\n    }\n\n    def handleSubmit(e: SyntheticEvent[_, dom.Event]): Unit = {\n      e.preventDefault()\n\n      if (formData.isSubmitButtonEnabled) {\n        setFormData(_.submit)\n        for {\n          request <- formData.data.submitRequest\n            .orElse {\n              setFormData(_.submissionFailed(texts.completeData))\n              None\n            }\n        } yield props.ctx.api.client\n          .forgotPassword(request)\n          .onComplete {\n            case Success(_) =>\n              setFormData(_.submitted)\n              history.push(\"/signin\") // redirects to sign in page\n\n            case Failure(ex) =>\n              setFormData(_.submissionFailed(ex.getMessage))\n          }\n      } else {\n        println(\"Submit fired when it is not available\")\n      }\n    }\n\n    val forgotPasswordButton = {\n      val text = if (formData.isSubmitting) {\n        Fragment(\n          CircularLoader(),\n          Container(margin = Container.EdgeInsets.left(8), child = texts.loading)\n        )\n      } else {\n        Fragment(texts.recover)\n      }\n\n      mui.Button\n        .normal()(text)\n        .fullWidth(true)\n        .disabled(formData.isSubmitButtonDisabled)\n        .variant(\"contained\")\n        .color(\"primary\")\n        .size(\"large\")\n        .`type`(\"submit\")\n    }\n\n    val emailInput = EmailInput\n      .component(\n        EmailInput.Props(\n          formData.data.email,\n          disabled = formData.isInputDisabled,\n          onChange = value => onDataChanged(x => x.copy(email = x.email.updated(value)))\n        )\n      )\n\n    val recaptcha = ReCaptcha(props.ctx, onChange = captchaOpt => onDataChanged(x => x.copy(captcha = captchaOpt)))\n\n    form(onSubmit := (handleSubmit(_)))(\n      Container(\n        margin = Container.EdgeInsets.all(16),\n        alignItems = Container.Alignment.center,\n        child = Fragment(\n          emailInput,\n          Container(\n            margin = Container.EdgeInsets.top(8),\n            child = recaptcha\n          ),\n          formData.firstValidationError.map { text =>\n            Container(\n              margin = Container.EdgeInsets.top(16),\n              child = ErrorLabel(text)\n            )\n          }\n        )\n      ),\n      Container(\n        alignItems = Container.Alignment.center,\n        child = forgotPasswordButton\n      )\n    )\n  }\n}\n"
  },
  {
    "path": "web/src/main/scala/net/wiringbits/components/widgets/Loader.scala",
    "content": "package net.wiringbits.components.widgets\n\nimport com.olvind.mui.muiMaterial.components as mui\nimport net.wiringbits.AppContext\nimport net.wiringbits.core.I18nHooks\nimport net.wiringbits.webapp.utils.slinkyUtils.components.core.widgets.{CircularLoader, Container}\nimport slinky.core.facade.Fragment\nimport slinky.core.{FunctionalComponent, KeyAddingStage}\n\nobject Loader {\n  def apply(ctx: AppContext): KeyAddingStage =\n    component(Props(ctx = ctx))\n\n  case class Props(ctx: AppContext)\n\n  val component: FunctionalComponent[Props] = FunctionalComponent[Props] { props =>\n    val texts = I18nHooks.useMessages(props.ctx.$lang)\n\n    Container(\n      flex = Some(1),\n      alignItems = Container.Alignment.center,\n      justifyContent = Container.Alignment.center,\n      child = Fragment(\n        CircularLoader(),\n        mui.Typography(texts.loading).variant(\"h6\")\n      )\n    )\n  }\n}\n"
  },
  {
    "path": "web/src/main/scala/net/wiringbits/components/widgets/LogList.scala",
    "content": "package net.wiringbits.components.widgets\n\nimport com.olvind.mui.csstype.mod.Property.FlexDirection\nimport com.olvind.mui.muiMaterial.components as mui\nimport com.olvind.mui.muiMaterial.mod.PropTypes.Color\nimport net.wiringbits.AppContext\nimport net.wiringbits.api.models.users.GetUserLogs\nimport net.wiringbits.api.utils.Formatter\nimport net.wiringbits.core.I18nHooks\nimport net.wiringbits.webapp.utils.slinkyUtils.Utils.CSSPropertiesUtils\nimport net.wiringbits.webapp.utils.slinkyUtils.components.core.widgets.{Container, Subtitle}\nimport slinky.core.facade.Fragment\nimport slinky.core.{FunctionalComponent, KeyAddingStage}\n\nobject LogList {\n  def apply(ctx: AppContext, response: GetUserLogs.Response, forceRefresh: () => Unit): KeyAddingStage =\n    component(Props(ctx = ctx, response = response, forceRefresh = forceRefresh))\n\n  case class Props(ctx: AppContext, response: GetUserLogs.Response, forceRefresh: () => Unit)\n  private val styling = new CSSPropertiesUtils {\n    width = \"100%\"\n  }\n\n  val component: FunctionalComponent[Props] = FunctionalComponent[Props] { props =>\n    val texts = I18nHooks.useMessages(props.ctx.$lang)\n    val items = props.response.data.map { item =>\n      mui.ListItem\n        .normal()(\n          mui\n            .ListItemText()\n            .primary(item.message)\n            .secondary(Formatter.instant(item.createdAt))\n        )\n        .divider(true)\n        .withKey(item.userLogId.toString)\n        .build\n    }\n\n    Container(\n      minWidth = Some(\"100%\"),\n      child = Fragment(\n        Container(\n          minWidth = Some(\"100%\"),\n          flexDirection = FlexDirection.row,\n          alignItems = Container.Alignment.center,\n          justifyContent = Container.Alignment.spaceBetween,\n          child = Fragment(\n            Subtitle(texts.logs),\n            mui.Button\n              .normal()(texts.reload)\n              .color(Color.primary)\n              .onClick(_ => props.forceRefresh())\n          )\n        ),\n        mui\n          .List(items)\n          .sx(styling)\n          .dense(true)\n      )\n    )\n  }\n}\n"
  },
  {
    "path": "web/src/main/scala/net/wiringbits/components/widgets/Logs.scala",
    "content": "package net.wiringbits.components.widgets\n\nimport net.wiringbits.AppContext\nimport net.wiringbits.api.models.users.GetUserLogs\nimport net.wiringbits.models.User\nimport net.wiringbits.webapp.utils.slinkyUtils.components.core.AsyncComponent\nimport net.wiringbits.webapp.utils.slinkyUtils.core.GenericHooks\nimport slinky.core.{FunctionalComponent, KeyAddingStage}\n\nobject Logs {\n  def apply(ctx: AppContext, user: User): KeyAddingStage =\n    component(Props(ctx = ctx, user = user))\n\n  case class Props(ctx: AppContext, user: User)\n\n  val component: FunctionalComponent[Props] = FunctionalComponent[Props] { props =>\n    val (timesRefreshingData, forceRefresh) = GenericHooks.useForceRefresh\n\n    AsyncComponent[GetUserLogs.Response](\n      fetch = () => props.ctx.api.client.getUserLogs,\n      render = response => LogList(props.ctx, response, () => forceRefresh()),\n      progressIndicator = () => Loader(props.ctx),\n      watchedObjects = List(timesRefreshingData)\n    )\n  }\n}\n"
  },
  {
    "path": "web/src/main/scala/net/wiringbits/components/widgets/ReCaptcha.scala",
    "content": "package net.wiringbits.components.widgets\n\nimport net.wiringbits.AppContext\nimport net.wiringbits.common.models.Captcha\nimport net.wiringbits.webapp.utils.slinkyUtils.components.core.AsyncComponent\nimport slinky.core.facade.Hooks\nimport slinky.core.{FunctionalComponent, KeyAddingStage}\nimport typings.reactGoogleRecaptcha.components.ReactGoogleRecaptcha\n\nimport scala.concurrent.ExecutionContext\n\nobject ReCaptcha {\n  def apply(ctx: AppContext, onChange: Option[Captcha] => Unit): KeyAddingStage =\n    component(Props(ctx = ctx, onChange = onChange))\n\n  case class Props(ctx: AppContext, onChange: Option[Captcha] => Unit)\n\n  val component: FunctionalComponent[Props] = FunctionalComponent[Props] { props =>\n    implicit val ec: ExecutionContext = props.ctx.executionContext\n\n    // Without useMemo, the component gets rendered everytime the captcha is solved\n    Hooks.useMemo(\n      () =>\n        AsyncComponent[String](\n          fetch = () => props.ctx.api.client.getEnvironmentConfig.map(_.recaptchaSiteKey),\n          render = recaptchaSiteKey =>\n            ReactGoogleRecaptcha(recaptchaSiteKey)\n              .onChange(x => props.onChange(Captcha.validate(x.asInstanceOf[String]).toOption))\n        ),\n      \"\"\n    )\n  }\n}\n"
  },
  {
    "path": "web/src/main/scala/net/wiringbits/components/widgets/ResendVerifyEmailForm.scala",
    "content": "package net.wiringbits.components.widgets\n\nimport com.olvind.mui.csstype.mod.Property.FlexDirection\nimport com.olvind.mui.muiMaterial.components as mui\nimport com.olvind.mui.muiMaterial.mod.PropTypes.Color\nimport com.olvind.mui.react.components.Fragment\nimport com.olvind.mui.react.mod.CSSProperties\nimport net.wiringbits.AppContext\nimport net.wiringbits.common.models.Email\nimport net.wiringbits.core.I18nHooks\nimport net.wiringbits.forms.ResendVerifyEmailFormData\nimport net.wiringbits.ui.components.inputs.EmailInput\nimport net.wiringbits.webapp.utils.slinkyUtils.components.core.ErrorLabel\nimport net.wiringbits.webapp.utils.slinkyUtils.components.core.widgets.{CircularLoader, Container, Title}\nimport net.wiringbits.webapp.utils.slinkyUtils.facades.reactrouterdom.useHistory\nimport net.wiringbits.webapp.utils.slinkyUtils.forms.StatefulFormData\nimport org.scalajs.dom\nimport org.scalajs.dom.URLSearchParams\nimport org.scalajs.macrotaskexecutor.MacrotaskExecutor.Implicits.global\nimport slinky.core.facade.Hooks\nimport slinky.core.{FunctionalComponent, KeyAddingStage, SyntheticEvent}\nimport slinky.web.html.{form, onSubmit}\n\nimport scala.scalajs.js\nimport scala.util.{Failure, Success}\n\nobject ResendVerifyEmailForm {\n  def apply(ctx: AppContext): KeyAddingStage =\n    component(Props(ctx = ctx))\n\n  case class Props(ctx: AppContext)\n\n  val component: FunctionalComponent[Props] = FunctionalComponent[Props] { props =>\n    val texts = I18nHooks.useMessages(props.ctx.$lang)\n    val history = useHistory()\n    val params = URLSearchParams(dom.window.location.search)\n    val emailParam = Option(params.get(\"email\")).getOrElse(\"\")\n    val (formData, setFormData) = Hooks.useState(\n      StatefulFormData(\n        ResendVerifyEmailFormData.initial(\n          ResendVerifyEmailFormData.Texts(texts.completeTheCaptcha),\n          emailLabel = texts.email,\n          emailValue = Some(Email.validate(emailParam))\n        )\n      )\n    )\n\n    def onDataChanged(f: ResendVerifyEmailFormData => ResendVerifyEmailFormData): Unit = {\n      setFormData { current =>\n        current.filling.copy(data = f(current.data))\n      }\n    }\n\n    def handleSubmit(e: SyntheticEvent[_, dom.Event]): Unit = {\n      e.preventDefault()\n\n      if (formData.isSubmitButtonEnabled) {\n        setFormData(_.submit)\n        for {\n          request <- formData.data.submitRequest\n            .orElse {\n              setFormData(_.submissionFailed(texts.completeData))\n              None\n            }\n        } yield props.ctx.api.client\n          .sendEmailVerificationToken(request)\n          .onComplete {\n            case Success(_) =>\n              val email = formData.data.email.inputValue\n\n              setFormData(_.submitted)\n              history.push(s\"/verify-email?email=${email}\")\n\n            case Failure(ex) =>\n              setFormData(_.submissionFailed(ex.getMessage))\n          }\n      } else {\n        println(\"Submit fired when it is not available\")\n      }\n    }\n\n    val emailInput = Container(\n      minWidth = Some(\"100%\"),\n      margin = Container.EdgeInsets.bottom(8),\n      child = EmailInput.component(\n        EmailInput.Props(\n          formData.data.email,\n          disabled = formData.isInputDisabled,\n          onChange = value => onDataChanged(x => x.copy(email = x.email.updated(value)))\n        )\n      )\n    )\n\n    val error = formData.firstValidationError.map { text =>\n      Container(\n        margin = Container.EdgeInsets.top(16),\n        child = ErrorLabel(text)\n      )\n    }\n\n    val recaptcha = ReCaptcha(props.ctx, onChange = captchaOpt => onDataChanged(x => x.copy(captcha = captchaOpt)))\n\n    val resendVerifyEmailButton = {\n      val text =\n        if (formData.isSubmitting) {\n          Fragment(\n            CircularLoader(),\n            Container(margin = Container.EdgeInsets.left(8), child = texts.loading)\n          )\n        } else Fragment(texts.resendEmail)\n\n      mui.Button\n        .normal()(text)\n        .fullWidth(true)\n        .disabled(formData.isSubmitButtonDisabled)\n        .variant(\"contained\")\n        .color(\"primary\")\n        .`type`(\"submit\")\n    }\n\n    form(onSubmit := (handleSubmit(_)))(\n      mui\n        .Paper()\n        .elevation(1)(\n          Container(\n            minWidth = Some(\"300px\"),\n            alignItems = Container.Alignment.center,\n            padding = Container.EdgeInsets.all(16),\n            child = Fragment(\n              Title(texts.resendEmail),\n              emailInput,\n              recaptcha,\n              error,\n              Container(\n                minWidth = Some(\"100%\"),\n                margin = Container.EdgeInsets.top(16),\n                alignItems = Container.Alignment.center,\n                child = resendVerifyEmailButton\n              )\n            )\n          )\n        )\n    )\n  }\n}\n"
  },
  {
    "path": "web/src/main/scala/net/wiringbits/components/widgets/ResetPasswordForm.scala",
    "content": "package net.wiringbits.components.widgets\n\nimport com.olvind.mui.muiMaterial.components as mui\nimport net.wiringbits.AppContext\nimport net.wiringbits.common.models.UserToken\nimport net.wiringbits.core.I18nHooks\nimport net.wiringbits.forms.ResetPasswordFormData\nimport net.wiringbits.models.User\nimport net.wiringbits.ui.components.inputs.PasswordInput\nimport net.wiringbits.webapp.utils.slinkyUtils.components.core.ErrorLabel\nimport net.wiringbits.webapp.utils.slinkyUtils.components.core.widgets.{CircularLoader, Container}\nimport net.wiringbits.webapp.utils.slinkyUtils.facades.reactrouterdom.useHistory\nimport net.wiringbits.webapp.utils.slinkyUtils.forms.StatefulFormData\nimport org.scalajs.dom\nimport org.scalajs.macrotaskexecutor.MacrotaskExecutor.Implicits.global\nimport slinky.core.facade.{Fragment, Hooks}\nimport slinky.core.{FunctionalComponent, KeyAddingStage, SyntheticEvent}\nimport slinky.web.html.{form, onSubmit}\n\nimport scala.scalajs.js\nimport scala.util.{Failure, Success}\n\nobject ResetPasswordForm {\n  def apply(ctx: AppContext, token: Option[UserToken]): KeyAddingStage =\n    component(Props(ctx = ctx, token = token))\n\n  case class Props(ctx: AppContext, token: Option[UserToken])\n\n  val component: FunctionalComponent[Props] = FunctionalComponent[Props] { props =>\n    val texts = I18nHooks.useMessages(props.ctx.$lang)\n    val history = useHistory()\n\n    val (formData, setFormData) = Hooks.useState(\n      StatefulFormData(\n        ResetPasswordFormData.initial(\n          passwordLabel = texts.password,\n          repeatPasswordLabel = texts.repeatPassword,\n          token = props.token\n        )\n      )\n    )\n\n    def onDataChanged(f: ResetPasswordFormData => ResetPasswordFormData): Unit = {\n      setFormData { current =>\n        current.filling.copy(data = f(current.data))\n      }\n    }\n\n    def handleSubmit(e: SyntheticEvent[_, dom.Event]): Unit = {\n      e.preventDefault()\n\n      if (formData.isSubmitButtonEnabled) {\n        setFormData(_.submit)\n        for {\n          request <- formData.data.submitRequest\n            .orElse {\n              setFormData(_.submissionFailed(texts.completeData))\n              None\n            }\n        } yield props.ctx.api.client\n          .resetPassword(request)\n          .onComplete {\n            case Success(res) =>\n              props.ctx.loggedIn(User(name = res.name, email = res.email))\n              setFormData(_.submitted)\n              history.push(\"/dashboard\")\n            case Failure(ex) =>\n              setFormData(_.submissionFailed(ex.getMessage))\n          }\n      } else {\n        println(\"Submit fired when it is not available\")\n      }\n    }\n\n    val passwordInput = PasswordInput\n      .component(\n        PasswordInput.Props(\n          formData.data.password,\n          disabled = formData.isInputDisabled,\n          onChange = value => onDataChanged(x => x.copy(password = x.password.updated(value)))\n        )\n      )\n\n    val repeatPasswordInput = PasswordInput\n      .component(\n        PasswordInput.Props(\n          formData.data.repeatPassword,\n          disabled = formData.isInputDisabled,\n          onChange = value => onDataChanged(x => x.copy(repeatPassword = x.repeatPassword.updated(value)))\n        )\n      )\n\n    val resetPasswordButton = {\n      val text = if (formData.isSubmitting) {\n        Fragment(\n          CircularLoader(),\n          Container(margin = Container.EdgeInsets.left(8), child = texts.loading)\n        )\n      } else {\n        Fragment(texts.resetPassword)\n      }\n\n      mui.Button\n        .normal()(text)\n        .fullWidth(true)\n        .disabled(formData.isSubmitButtonDisabled)\n        .variant(\"contained\")\n        .color(\"primary\")\n        .size(\"large\")\n        .`type`(\"submit\")\n    }\n\n    form(onSubmit := (handleSubmit(_)))(\n      Container(\n        margin = Container.EdgeInsets.all(16),\n        alignItems = Container.Alignment.center,\n        child = Fragment(\n          passwordInput,\n          repeatPasswordInput,\n          formData.firstValidationError.map { text =>\n            Container(\n              margin = Container.EdgeInsets.top(16),\n              child = ErrorLabel(text)\n            )\n          }\n        )\n      ),\n      Container(\n        alignItems = Container.Alignment.center,\n        child = Fragment(\n          resetPasswordButton\n        )\n      )\n    )\n  }\n}\n"
  },
  {
    "path": "web/src/main/scala/net/wiringbits/components/widgets/SignInForm.scala",
    "content": "package net.wiringbits.components.widgets\n\nimport com.olvind.mui.muiMaterial.components as mui\nimport com.olvind.mui.muiMaterial.mod.PropTypes.Color\nimport net.wiringbits.AppContext\nimport net.wiringbits.common.ErrorMessages\nimport net.wiringbits.core.I18nHooks\nimport net.wiringbits.forms.SignInFormData\nimport net.wiringbits.models.User\nimport net.wiringbits.ui.components.inputs.{EmailInput, PasswordInput}\nimport net.wiringbits.webapp.utils.slinkyUtils.components.core.ErrorLabel\nimport net.wiringbits.webapp.utils.slinkyUtils.components.core.widgets.{CircularLoader, Container}\nimport net.wiringbits.webapp.utils.slinkyUtils.facades.reactrouterdom.useHistory\nimport net.wiringbits.webapp.utils.slinkyUtils.forms.StatefulFormData\nimport org.scalajs.dom\nimport org.scalajs.macrotaskexecutor.MacrotaskExecutor.Implicits.*\nimport slinky.core.facade.{Fragment, Hooks, ReactElement}\nimport slinky.core.{FunctionalComponent, KeyAddingStage, SyntheticEvent}\nimport slinky.web.html.{form, onSubmit}\n\nimport scala.scalajs.js\nimport scala.util.{Failure, Success}\n\nobject SignInForm {\n  def apply(ctx: AppContext): KeyAddingStage =\n    component(Props(ctx = ctx))\n\n  case class Props(ctx: AppContext)\n\n  val component: FunctionalComponent[Props] = FunctionalComponent[Props] { props =>\n    val texts = I18nHooks.useMessages(props.ctx.$lang)\n    val history = useHistory()\n    val (formData, setFormData) = Hooks.useState(\n      StatefulFormData(\n        SignInFormData.initial(\n          emailLabel = texts.email,\n          passwordLabel = texts.password\n        )\n      )\n    )\n\n    def onDataChanged(f: SignInFormData => SignInFormData): Unit = {\n      setFormData { current =>\n        current.filling.copy(data = f(current.data))\n      }\n    }\n\n    def handleSubmit(e: SyntheticEvent[_, dom.Event]): Unit = {\n      e.preventDefault()\n\n      if (formData.isSubmitButtonEnabled) {\n        setFormData(_.submit)\n        for {\n          request <- formData.data.submitRequest\n            .orElse {\n              setFormData(_.submissionFailed(texts.completeData))\n              None\n            }\n        } yield props.ctx.api.client\n          .login(request)\n          .onComplete {\n            case Success(res) =>\n              setFormData(_.submitted)\n              props.ctx.loggedIn(User(res.name, res.email))\n              history.push(\"/dashboard\") // redirects to the dashboard\n\n            case Failure(ex) =>\n              setFormData(_.submissionFailed(ex.getMessage))\n          }\n      } else {\n        println(\"Submit fired when it is not available\")\n      }\n    }\n\n    val emailInput = Container(\n      minWidth = Some(\"100%\"),\n      margin = Container.EdgeInsets.bottom(8),\n      child = EmailInput\n        .component(\n          EmailInput.Props(\n            formData.data.email,\n            disabled = formData.isInputDisabled,\n            onChange = value => onDataChanged(x => x.copy(email = x.email.updated(value)))\n          )\n        )\n    )\n\n    val passwordInput = Container(\n      minWidth = Some(\"100%\"),\n      margin = Container.EdgeInsets.bottom(16),\n      child = PasswordInput\n        .component(\n          PasswordInput.Props(\n            formData.data.password,\n            disabled = formData.isInputDisabled,\n            onChange = value => onDataChanged(x => x.copy(password = x.password.updated(value)))\n          )\n        )\n    )\n\n    def resendVerifyEmailButton(text: String): ReactElement = {\n      // TODO: It would be ideal to match the error against a code than matching a text\n      text match {\n        case ErrorMessages.`emailNotVerified` =>\n          val email = formData.data.email.inputValue\n\n          mui.Button\n            .normal()(texts.resendEmail)\n            .variant(\"text\")\n            .color(\"primary\")\n            .onClick(_ => history.push(s\"/resend-verify-email?email=${email}\"))\n        case _ => Fragment()\n      }\n    }\n\n    val error = formData.firstValidationError.map { errorMessage =>\n      Container(\n        alignItems = Container.Alignment.center,\n        margin = Container.EdgeInsets.top(16),\n        child = Fragment(\n          ErrorLabel(errorMessage),\n          resendVerifyEmailButton(errorMessage)\n        )\n      )\n    }\n\n    val recaptcha = ReCaptcha(props.ctx, onChange = captchaOpt => onDataChanged(x => x.copy(captcha = captchaOpt)))\n\n    val loginButton = {\n      val text =\n        if (formData.isSubmitting)\n          Fragment(\n            CircularLoader(),\n            Container(margin = Container.EdgeInsets.left(8), child = texts.loading)\n          )\n        else\n          Fragment(texts.login)\n\n      mui.Button\n        .normal()(text)\n        .fullWidth(true)\n        .disabled(formData.isSubmitButtonDisabled)\n        .variant(\"contained\")\n        .color(\"primary\")\n        .`type`(\"submit\")\n    }\n\n    form(onSubmit := (handleSubmit(_)))(\n      Container(\n        alignItems = Container.Alignment.center,\n        justifyContent = Container.Alignment.center,\n        child = Fragment(\n          emailInput,\n          passwordInput,\n          recaptcha,\n          error,\n          Container(\n            minWidth = Some(\"100%\"),\n            margin = Container.EdgeInsets.top(16),\n            alignItems = Container.Alignment.center,\n            child = loginButton\n          )\n        )\n      )\n    )\n  }\n}\n"
  },
  {
    "path": "web/src/main/scala/net/wiringbits/components/widgets/SignUpForm.scala",
    "content": "package net.wiringbits.components.widgets\n\nimport com.olvind.mui.muiMaterial.components as mui\nimport net.wiringbits.AppContext\nimport net.wiringbits.core.I18nHooks\nimport net.wiringbits.forms.SignUpFormData\nimport net.wiringbits.ui.components.inputs.{EmailInput, NameInput, PasswordInput}\nimport net.wiringbits.webapp.utils.slinkyUtils.components.core.ErrorLabel\nimport net.wiringbits.webapp.utils.slinkyUtils.components.core.widgets.{CircularLoader, Container, Title}\nimport net.wiringbits.webapp.utils.slinkyUtils.facades.reactrouterdom.useHistory\nimport net.wiringbits.webapp.utils.slinkyUtils.forms.StatefulFormData\nimport org.scalajs.dom\nimport org.scalajs.macrotaskexecutor.MacrotaskExecutor.Implicits.global\nimport slinky.core.facade.{Fragment, Hooks}\nimport slinky.core.{FunctionalComponent, KeyAddingStage, SyntheticEvent}\nimport slinky.web.html.{form, onSubmit}\n\nimport scala.scalajs.js\nimport scala.util.{Failure, Success}\n\nobject SignUpForm {\n  def apply(ctx: AppContext): KeyAddingStage =\n    component(Props(ctx = ctx))\n\n  case class Props(ctx: AppContext)\n\n  val component: FunctionalComponent[Props] = FunctionalComponent[Props] { props =>\n    val texts = I18nHooks.useMessages(props.ctx.$lang)\n    val history = useHistory()\n    val (formData, setFormData) = Hooks.useState(\n      StatefulFormData(\n        SignUpFormData.initial(\n          nameLabel = texts.name,\n          emailLabel = texts.email,\n          passwordLabel = texts.password,\n          repeatPasswordLabel = texts.repeatPassword\n        )\n      )\n    )\n\n    def onDataChanged(f: SignUpFormData => SignUpFormData): Unit = {\n      setFormData { current =>\n        current.filling.copy(data = f(current.data))\n      }\n    }\n\n    def handleSubmit(e: SyntheticEvent[_, dom.Event]): Unit = {\n      e.preventDefault()\n\n      if (formData.isSubmitButtonEnabled) {\n        setFormData(_.submit)\n        for {\n          request <- formData.data.submitRequest\n            .orElse {\n              setFormData(_.submissionFailed(texts.completeData))\n              None\n            }\n        } yield props.ctx.api.client\n          .createUser(request)\n          .onComplete {\n            case Success(_) =>\n              val email = formData.data.email.inputValue\n\n              setFormData(_.submitted)\n              history.push(s\"/verify-email?email=$email\") // redirects to email page\n\n            case Failure(ex) =>\n              setFormData(_.submissionFailed(ex.getMessage))\n          }\n      } else {\n        println(\"Submit fired when it is not available\")\n      }\n    }\n\n    val nameInput = Container(\n      minWidth = Some(\"100%\"),\n      margin = Container.EdgeInsets.bottom(8),\n      child = NameInput.component(\n        NameInput.Props(\n          formData.data.name,\n          disabled = formData.isInputDisabled,\n          onChange = value => onDataChanged(x => x.copy(name = x.name.updated(value)))\n        )\n      )\n    )\n\n    val emailInput = Container(\n      minWidth = Some(\"100%\"),\n      margin = Container.EdgeInsets.bottom(8),\n      child = EmailInput.component(\n        EmailInput.Props(\n          formData.data.email,\n          disabled = formData.isInputDisabled,\n          onChange = value => onDataChanged(x => x.copy(email = x.email.updated(value)))\n        )\n      )\n    )\n\n    val passwordInput = Container(\n      minWidth = Some(\"100%\"),\n      margin = Container.EdgeInsets.bottom(16),\n      child = PasswordInput\n        .component(\n          PasswordInput.Props(\n            formData.data.password,\n            disabled = formData.isInputDisabled,\n            onChange = value => onDataChanged(x => x.copy(password = x.password.updated(value)))\n          )\n        )\n    )\n\n    val repeatPasswordInput = Container(\n      minWidth = Some(\"100%\"),\n      margin = Container.EdgeInsets.bottom(16),\n      child = PasswordInput\n        .component(\n          PasswordInput.Props(\n            formData.data.repeatPassword,\n            disabled = formData.isInputDisabled,\n            onChange = value => onDataChanged(x => x.copy(repeatPassword = x.repeatPassword.updated(value)))\n          )\n        )\n    )\n\n    val error = formData.firstValidationError.map { text =>\n      Container(\n        margin = Container.EdgeInsets.top(16),\n        child = ErrorLabel(text)\n      )\n    }\n\n    val recaptcha = ReCaptcha(props.ctx, onChange = captchaOpt => onDataChanged(x => x.copy(captcha = captchaOpt)))\n\n    val signUpButton = {\n      val text =\n        if (formData.isSubmitting) {\n          Fragment(\n            CircularLoader(),\n            Container(margin = Container.EdgeInsets.left(8), child = texts.loading)\n          )\n        } else Fragment(texts.createAccount)\n\n      mui.Button\n        .normal()(text)\n        .fullWidth(true)\n        .disabled(formData.isSubmitButtonDisabled)\n        .variant(\"contained\")\n        .color(\"primary\")\n        .`type`(\"submit\")\n    }\n\n    // TODO: Use a form to get the enter key submitting the form\n    form(onSubmit := (handleSubmit(_)))(\n      mui\n        .Paper()\n        .elevation(1)(\n          Container(\n            minWidth = Some(\"300px\"),\n            alignItems = Container.Alignment.center,\n            padding = Container.EdgeInsets.all(16),\n            child = Fragment(\n              Title(texts.signUp),\n              nameInput,\n              emailInput,\n              passwordInput,\n              repeatPasswordInput,\n              recaptcha,\n              error,\n              Container(\n                minWidth = Some(\"100%\"),\n                margin = Container.EdgeInsets.top(16),\n                alignItems = Container.Alignment.center,\n                child = signUpButton\n              )\n            )\n          )\n        )\n    )\n  }\n}\n"
  },
  {
    "path": "web/src/main/scala/net/wiringbits/components/widgets/UserInfo.scala",
    "content": "package net.wiringbits.components.widgets\n\nimport com.olvind.mui.muiMaterial.components as mui\nimport net.wiringbits.AppContext\nimport net.wiringbits.api.models.auth.GetCurrentUser\nimport net.wiringbits.core.I18nHooks\nimport net.wiringbits.models.User\nimport net.wiringbits.webapp.utils.slinkyUtils.components.core.AsyncComponent\nimport net.wiringbits.webapp.utils.slinkyUtils.components.core.widgets.{CircularLoader, Container}\nimport slinky.core.facade.Fragment\nimport slinky.core.{FunctionalComponent, KeyAddingStage}\n\nobject UserInfo {\n  def apply(ctx: AppContext, user: User): KeyAddingStage =\n    component(Props(ctx = ctx, user = user))\n\n  case class Props(ctx: AppContext, user: User)\n\n  val component: FunctionalComponent[Props] = FunctionalComponent[Props] { props =>\n    val texts = I18nHooks.useMessages(props.ctx.$lang)\n    def loader = Container(\n      flex = Some(1),\n      alignItems = Container.Alignment.center,\n      justifyContent = Container.Alignment.center,\n      child = Fragment(\n        CircularLoader(48),\n        mui.Typography(texts.loading).variant(\"h4\").color(\"primary\")\n      )\n    )\n\n    def onSaveClick(): Unit = {\n      renderBody()\n    }\n\n    def renderBody() = {\n      AsyncComponent[GetCurrentUser.Response](\n        fetch = () => props.ctx.api.client.currentUser,\n        render = response => EditUserForm(props.ctx, props.user, response, onSaveClick),\n        progressIndicator = () => loader\n      )\n    }\n\n    renderBody()\n  }\n}\n"
  },
  {
    "path": "web/src/main/scala/net/wiringbits/core/I18nHooks.scala",
    "content": "package net.wiringbits.core\n\nimport monix.reactive.subjects.Var\nimport net.wiringbits.I18nMessages\nimport slinky.core.facade.Hooks\n\nobject I18nHooks {\n  def useMessages($lang: Var[I18nLang]): I18nMessages = {\n    val lang = ReactiveHooks.useDistinctValue($lang)\n    Hooks.useMemo(() => new I18nMessages(lang), List(lang))\n  }\n}\n"
  },
  {
    "path": "web/src/main/scala/net/wiringbits/core/I18nLang.scala",
    "content": "package net.wiringbits.core\n\nsealed trait I18nLang extends Product with Serializable\n\nobject I18nLang {\n  case object English extends I18nLang\n\n  val values: List[I18nLang] = List(English)\n\n  def from(string: String): Option[I18nLang] = {\n    values.find(_.toString.toLowerCase == string.toLowerCase)\n  }\n\n  implicit val catsEq: cats.Eq[I18nLang] = cats.Eq.fromUniversalEquals\n}\n"
  },
  {
    "path": "web/src/main/scala/net/wiringbits/core/ReactiveHooks.scala",
    "content": "package net.wiringbits.core\n\nimport monix.reactive.subjects.Var\nimport slinky.core.facade.Hooks\n\nobject ReactiveHooks {\n\n  import monix.execution.Scheduler.Implicits.global\n\n  /** Gets the value from a monix Var, and, updates the state when the Var gets new values\n    */\n  def useValue[T](value: Var[T]): T = {\n    val (state, setState) = Hooks.useState[T](value())\n    Hooks.useEffect(\n      () => {\n        val cancelable = value.foreach(setState.apply)\n        () => cancelable.cancel()\n      },\n      List(value)\n    )\n    state\n  }\n\n  /** Gets the value from a monix Var, and, updates the state only when it gets a different value\n    */\n  def useDistinctValue[T](value: Var[T])(implicit A: cats.Eq[T]): T = {\n    val (state, setState) = Hooks.useState[T](value())\n    Hooks.useEffect(\n      () => {\n        val cancelable = value.distinctUntilChanged.foreach(setState.apply)\n        () => cancelable.cancel()\n      },\n      List(value)\n    )\n    state\n  }\n}\n"
  },
  {
    "path": "web/src/main/scala/net/wiringbits/forms/ForgotPasswordFormData.scala",
    "content": "package net.wiringbits.forms\n\nimport net.wiringbits.api.models.users.ForgotPassword\nimport net.wiringbits.common.models.{Captcha, Email}\nimport net.wiringbits.webapp.utils.slinkyUtils.forms.{FormData, FormField}\n\ncase class ForgotPasswordFormData(\n    email: FormField[Email],\n    captcha: Option[Captcha] = None\n) extends FormData[ForgotPassword.Request] {\n  override def fields: List[FormField[_]] = List(email)\n\n  override def formValidationErrors: List[String] = {\n    val captchaError = Option.when(captcha.isEmpty)(\"Complete the captcha\")\n\n    List(\n      fieldsError,\n      captchaError\n    ).flatten\n  }\n\n  override def submitRequest: Option[ForgotPassword.Request] = {\n    val formData = this\n    for {\n      email <- formData.email.valueOpt\n      captcha <- formData.captcha\n    } yield ForgotPassword.Request(\n      email = email,\n      captcha = captcha\n    )\n  }\n}\n\nobject ForgotPasswordFormData {\n  // TODO: Implement \"Complete captcha message\" from i18nMessages like ResendVerifyEmailFormData\n\n  def initial(\n      emailLabel: String\n  ): ForgotPasswordFormData = ForgotPasswordFormData(\n    email = new FormField(label = emailLabel, name = \"email\", required = true, `type` = \"email\")\n  )\n}\n"
  },
  {
    "path": "web/src/main/scala/net/wiringbits/forms/ResendVerifyEmailFormData.scala",
    "content": "package net.wiringbits.forms\n\nimport net.wiringbits.api.models.users.SendEmailVerificationToken\nimport net.wiringbits.common.models.{Captcha, Email}\nimport net.wiringbits.webapp.common.validators.ValidationResult\nimport net.wiringbits.webapp.utils.slinkyUtils.forms.{FormData, FormField}\n\ncase class ResendVerifyEmailFormData(\n    texts: ResendVerifyEmailFormData.Texts,\n    email: FormField[Email],\n    captcha: Option[Captcha] = None\n) extends FormData[SendEmailVerificationToken.Request] {\n  override def fields: List[FormField[_]] = List(email)\n\n  override def formValidationErrors: List[String] = {\n    val emptyCaptcha = Option.when(captcha.isEmpty)(texts.emptyCaptchaError)\n\n    List(\n      fieldsError,\n      emptyCaptcha\n    ).flatten\n  }\n\n  override def submitRequest: Option[SendEmailVerificationToken.Request] = {\n    val formData = this\n    for {\n      email <- formData.email.valueOpt\n      captcha <- formData.captcha\n    } yield SendEmailVerificationToken.Request(\n      email,\n      captcha\n    )\n  }\n}\n\nobject ResendVerifyEmailFormData {\n  case class Texts(emptyCaptchaError: String)\n\n  def initial(\n      texts: ResendVerifyEmailFormData.Texts,\n      emailLabel: String,\n      emailValue: Option[ValidationResult[Email]] = None\n  ): ResendVerifyEmailFormData = ResendVerifyEmailFormData(\n    texts = texts,\n    email = new FormField[Email](\n      label = emailLabel,\n      name = \"email\",\n      required = true,\n      `type` = \"email\",\n      value = emailValue\n    )\n  )\n}\n"
  },
  {
    "path": "web/src/main/scala/net/wiringbits/forms/ResetPasswordFormData.scala",
    "content": "package net.wiringbits.forms\n\nimport net.wiringbits.api.models.users.ResetPassword\nimport net.wiringbits.common.models.{Password, UserToken}\nimport net.wiringbits.webapp.utils.slinkyUtils.forms.{FormData, FormField}\n\ncase class ResetPasswordFormData(\n    password: FormField[Password],\n    repeatPassword: FormField[Password],\n    token: Option[UserToken]\n) extends FormData[ResetPassword.Request] {\n  override def fields: List[FormField[_]] = List(password, repeatPassword)\n\n  override def formValidationErrors: List[String] = {\n    val isTokenDefined =\n      Option.when(token.isEmpty)(\"The token doesn't exists\")\n\n    // the error is rendered only when both fields are provided\n    val passwordMatchesError = (for {\n      password1 <- password.valueOpt\n      password2 <- repeatPassword.valueOpt\n    } yield password1 != password2)\n      .filter(identity)\n      .map(_ => \"The passwords does not match\")\n\n    List(\n      fieldsError,\n      passwordMatchesError,\n      isTokenDefined\n    ).flatten\n  }\n\n  override def submitRequest: Option[ResetPassword.Request] = {\n    val formData = this\n    for {\n      password <- formData.password.valueOpt\n      token <- formData.token\n    } yield ResetPassword.Request(\n      token = token,\n      password = password\n    )\n  }\n}\n\nobject ResetPasswordFormData {\n\n  def initial(\n      passwordLabel: String,\n      repeatPasswordLabel: String,\n      token: Option[UserToken]\n  ): ResetPasswordFormData = ResetPasswordFormData(\n    password = new FormField(label = passwordLabel, name = \"password\", required = true, `type` = \"password\"),\n    repeatPassword =\n      new FormField(label = repeatPasswordLabel, name = \"repeatPassword\", required = true, `type` = \"password\"),\n    token = token\n  )\n}\n"
  },
  {
    "path": "web/src/main/scala/net/wiringbits/forms/SignInFormData.scala",
    "content": "package net.wiringbits.forms\n\nimport net.wiringbits.api.models.auth.Login\nimport net.wiringbits.common.models.{Captcha, Email, Password}\nimport net.wiringbits.webapp.utils.slinkyUtils.forms.{FormData, FormField}\n\ncase class SignInFormData(\n    email: FormField[Email],\n    password: FormField[Password],\n    captcha: Option[Captcha] = None\n) extends FormData[Login.Request] {\n  override def fields: List[FormField[_]] = List(email, password)\n\n  override def formValidationErrors: List[String] = {\n    val emptyCaptcha = Option.when(captcha.isEmpty)(\"Complete the captcha\")\n\n    List(\n      fieldsError,\n      emptyCaptcha\n    ).flatten\n  }\n\n  override def submitRequest: Option[Login.Request] = {\n    val formData = this\n    for {\n      email <- formData.email.valueOpt\n      password <- formData.password.valueOpt\n      captcha <- formData.captcha\n    } yield Login.Request(\n      email,\n      password,\n      captcha\n    )\n  }\n}\n\nobject SignInFormData {\n  // TODO: Implement \"Complete captcha message\" from i18nMessages like ResendVerifyEmailFormData\n\n  def initial(\n      emailLabel: String,\n      passwordLabel: String\n  ): SignInFormData = SignInFormData(\n    email = new FormField(label = emailLabel, name = \"email\", required = true, `type` = \"email\"),\n    password = new FormField(label = passwordLabel, name = \"password\", required = true, `type` = \"password\")\n  )\n}\n"
  },
  {
    "path": "web/src/main/scala/net/wiringbits/forms/SignUpFormData.scala",
    "content": "package net.wiringbits.forms\n\nimport net.wiringbits.api.models.users.CreateUser\nimport net.wiringbits.common.models.{Captcha, Email, Name, Password}\nimport net.wiringbits.webapp.utils.slinkyUtils.forms.{FormData, FormField}\n\ncase class SignUpFormData(\n    name: FormField[Name],\n    email: FormField[Email],\n    password: FormField[Password],\n    repeatPassword: FormField[Password],\n    captcha: Option[Captcha] = None\n) extends FormData[CreateUser.Request] {\n  override def fields: List[FormField[_]] = List(name, email, password, repeatPassword)\n\n  override def formValidationErrors: List[String] = {\n    // the error is rendered only when both fields are provided\n    val passwordMatchesError = (for {\n      password1 <- password.valueOpt\n      password2 <- repeatPassword.valueOpt\n    } yield password1 != password2)\n      .filter(identity)\n      .map(_ => \"The passwords does not match\")\n\n    val emptyCaptcha = Option.when(captcha.isEmpty)(\"Complete the captcha\")\n\n    List(\n      fieldsError,\n      passwordMatchesError,\n      emptyCaptcha\n    ).flatten\n  }\n\n  override def submitRequest: Option[CreateUser.Request] = {\n    val formData = this\n    for {\n      name <- formData.name.valueOpt\n      email <- formData.email.valueOpt\n      password <- formData.password.valueOpt\n      captcha <- formData.captcha\n    } yield CreateUser.Request(\n      name,\n      email,\n      password,\n      captcha\n    )\n  }\n}\n\nobject SignUpFormData {\n  // TODO: Implement \"Complete captcha message\" from i18nMessages like ResendVerifyEmailFormData\n\n  def initial(\n      nameLabel: String,\n      emailLabel: String,\n      passwordLabel: String,\n      repeatPasswordLabel: String\n  ): SignUpFormData = SignUpFormData(\n    name = new FormField(label = nameLabel, name = \"name\", required = true),\n    email = new FormField(label = emailLabel, name = \"email\", required = true, `type` = \"email\"),\n    password = new FormField(label = passwordLabel, name = \"password\", required = true, `type` = \"password\"),\n    repeatPassword =\n      new FormField(label = repeatPasswordLabel, name = \"repeatPassword\", required = true, `type` = \"password\")\n  )\n}\n"
  },
  {
    "path": "web/src/main/scala/net/wiringbits/forms/UpdateInfoFormData.scala",
    "content": "package net.wiringbits.forms\n\nimport net.wiringbits.api.models.users.UpdateUser\nimport net.wiringbits.common.models.*\nimport net.wiringbits.webapp.common.validators.ValidationResult\nimport net.wiringbits.webapp.utils.slinkyUtils.forms.{FormData, FormField}\n\ncase class UpdateInfoFormData(\n    name: FormField[Name],\n    email: FormField[Email]\n) extends FormData[UpdateUser.Request] {\n  override def fields: List[FormField[_]] = List(name)\n\n  override def formValidationErrors: List[String] = {\n    List(\n      fieldsError\n    ).flatten\n  }\n\n  override def submitRequest: Option[UpdateUser.Request] = {\n    val formData = this\n    for {\n      name <- formData.name.valueOpt\n    } yield UpdateUser.Request(\n      name\n    )\n  }\n}\n\nobject UpdateInfoFormData {\n\n  def initial(\n      nameLabel: String,\n      nameInitialValue: Option[Name] = None,\n      emailLabel: String,\n      emailValue: Option[Email] = None\n  ): UpdateInfoFormData = UpdateInfoFormData(\n    name = new FormField(\n      label = nameLabel,\n      name = \"name\",\n      value = nameInitialValue.map(x => ValidationResult.Valid(x.string, x))\n    ),\n    email = new FormField(\n      label = emailLabel,\n      name = \"email\",\n      `type` = \"email\",\n      value = emailValue.map(x => ValidationResult.Valid(x.string, x))\n    )\n  )\n}\n"
  },
  {
    "path": "web/src/main/scala/net/wiringbits/forms/UpdatePasswordFormData.scala",
    "content": "package net.wiringbits.forms\n\nimport net.wiringbits.api.models.users.UpdatePassword\nimport net.wiringbits.common.models.*\nimport net.wiringbits.webapp.utils.slinkyUtils.forms.{FormData, FormField}\n\ncase class UpdatePasswordFormData(\n    oldPassword: FormField[Password],\n    password: FormField[Password],\n    repeatPassword: FormField[Password]\n) extends FormData[UpdatePassword.Request] {\n  override def fields: List[FormField[_]] = List(oldPassword, password, repeatPassword)\n\n  override def formValidationErrors: List[String] = {\n    // the error is rendered only when both fields are provided\n    val passwordMatchesError = (for {\n      password1 <- password.valueOpt\n      password2 <- repeatPassword.valueOpt\n    } yield password1 != password2)\n      .filter(identity)\n      .map(_ => \"The passwords does not match\")\n\n    List(\n      fieldsError,\n      passwordMatchesError\n    ).flatten\n  }\n\n  override def submitRequest: Option[UpdatePassword.Request] = {\n    val formData = this\n    for {\n      oldPassword <- formData.oldPassword.valueOpt\n      password <- formData.password.valueOpt\n    } yield UpdatePassword.Request(\n      oldPassword,\n      password\n    )\n  }\n}\n\nobject UpdatePasswordFormData {\n\n  def initial(\n      oldPasswordLabel: String,\n      passwordLabel: String,\n      repeatPasswordLabel: String\n  ): UpdatePasswordFormData = UpdatePasswordFormData(\n    oldPassword = new FormField(label = oldPasswordLabel, name = \"oldPassword\", required = true, `type` = \"password\"),\n    password = new FormField(label = passwordLabel, name = \"password\", required = true, `type` = \"password\"),\n    repeatPassword =\n      new FormField(label = repeatPasswordLabel, name = \"repeatPassword\", required = true, `type` = \"password\")\n  )\n}\n"
  },
  {
    "path": "web/src/main/scala/net/wiringbits/models/AuthState.scala",
    "content": "package net.wiringbits.models\n\nsealed trait AuthState extends Product with Serializable\n\nobject AuthState {\n  case object Unauthenticated extends AuthState\n  case class Authenticated(user: User) extends AuthState\n\n  implicit val authStateEq: cats.Eq[AuthState] = cats.Eq.fromUniversalEquals\n}\n"
  },
  {
    "path": "web/src/main/scala/net/wiringbits/models/User.scala",
    "content": "package net.wiringbits.models\n\nimport net.wiringbits.common.models.{Email, Name}\n\ncase class User(name: Name, email: Email)\n"
  },
  {
    "path": "web/src/main/scala/net/wiringbits/models/UserMenuOption.scala",
    "content": "package net.wiringbits.models\n\nimport enumeratum.{Enum, EnumEntry}\n\nsealed abstract class UserMenuOption extends EnumEntry with Product with Serializable\n\nobject UserMenuOption extends Enum[UserMenuOption] {\n\n  case object EditSummary extends UserMenuOption\n  case object EditPassword extends UserMenuOption\n\n  val values = findValues\n}\n"
  },
  {
    "path": "web/src/main/scala/net/wiringbits/services/StorageService.scala",
    "content": "package net.wiringbits.services\n\nimport net.wiringbits.core.I18nLang\nimport org.scalajs.dom\n\nclass StorageService {\n  def saveLang(lang: I18nLang): Unit = save(\"lang\", lang.toString)\n  def findLang(): Option[I18nLang] = find(\"lang\").flatMap(I18nLang.from)\n\n  private def save(key: String, value: String): Unit = {\n    dom.window.localStorage.setItem(key, value)\n  }\n\n  private def find(key: String): Option[String] = {\n    Option(dom.window.localStorage.getItem(key))\n      .filter(_.nonEmpty)\n  }\n}\n"
  },
  {
    "path": "web/src/test/scala/java/security/SecureRandom.scala",
    "content": "/*\n * scalajs-fake-insecure-java-securerandom (https://github.com/scala-js/scala-js-fake-insecure-java-securerandom)\n *\n * Copyright EPFL.\n *\n * Licensed under Apache License 2.0\n * (https://www.apache.org/licenses/LICENSE-2.0).\n *\n * See the NOTICE file distributed with this work for\n * additional information regarding copyright ownership.\n */\n\npackage java.security\n\nimport scala.scalajs.js\nimport scala.scalajs.js.typedarray.*\n\n// DISCLAIMER: This is almost identical to the official library https://github.com/scala-js/scala-js-fake-insecure-java-securerandom\n// There was the need to apply a patch that won't be accepted by the upstream library, given that this is used\n// only for tests, it shouldn't be a problem to keep the patch.\n//\n// The seed in java.util.Random will be unused, so set to 0L instead of having to generate one\nclass SecureRandom() extends java.util.Random(0L) {\n  // Make sure to resolve the appropriate function no later than the first instantiation\n  private val getRandomValuesFun = SecureRandom.getRandomValuesFun\n\n  /* setSeed has no effect. For cryptographically secure PRNGs, giving a seed\n   * can only ever increase the entropy. It is never allowed to decrease it.\n   * Given that we don't have access to an API to strengthen the entropy of the\n   * underlying PRNG, it's fine to ignore it instead.\n   *\n   * Note that the doc of `SecureRandom` says that it will seed itself upon\n   * first call to `nextBytes` or `next`, if it has not been seeded yet. This\n   * suggests that an *initial* call to `setSeed` would make a `SecureRandom`\n   * instance deterministic. Experimentally, this does not seem to be the case,\n   * however, so we don't spend extra effort to make that happen.\n   */\n  override def setSeed(x: Long): Unit = ()\n\n  override def nextBytes(bytes: Array[Byte]): Unit = {\n    val len = bytes.length\n    val buffer = new Int8Array(len)\n    getRandomValuesFun(buffer)\n    var i = 0\n    while (i != len) {\n      bytes(i) = buffer(i)\n      i += 1\n    }\n  }\n\n  override protected final def next(numBits: Int): Int = {\n    if (numBits <= 0) {\n      0 // special case because the formula on the last line is incorrect for numBits == 0\n    } else {\n      val buffer = new Int32Array(1)\n      getRandomValuesFun(buffer)\n      val rand32 = buffer(0)\n      rand32 & (-1 >>> (32 - numBits)) // Clear the (32 - numBits) higher order bits\n    }\n  }\n}\n\nobject SecureRandom {\n  private lazy val getRandomValuesFun: js.Function1[ArrayBufferView, Unit] = {\n    if (\n      js.typeOf(js.Dynamic.global.crypto) != \"undefined\" &&\n      js.typeOf(js.Dynamic.global.crypto.getRandomValues) == \"function\"\n    ) {\n      { (buffer: ArrayBufferView) =>\n        js.Dynamic.global.crypto.getRandomValues(buffer)\n        ()\n      }\n    } else if (js.typeOf(js.Dynamic.global.require) == \"function\") {\n      try {\n        val crypto = js.Dynamic.global.require(\"crypto\")\n        if (js.typeOf(crypto.randomFillSync) == \"function\") {\n          { (buffer: ArrayBufferView) =>\n            /** This part differs from the official implementation because it catches runtime exceptions\n              *\n              * This was necessary because webpack seems to be polluting our runtime libraries with one that breaks in\n              * the tests.\n              */\n            try {\n              crypto.randomFillSync(buffer)\n            } catch {\n              case _: Throwable => insecureDefault(buffer)\n            }\n            ()\n          }\n        } else {\n          insecureDefault\n        }\n      } catch {\n        case _: Throwable =>\n          insecureDefault\n      }\n    } else {\n      insecureDefault\n    }\n  }\n\n  private def insecureDefault: js.Function1[ArrayBufferView, Unit] = {\n    val insecureRandom = new java.util.Random()\n\n    { (buffer: ArrayBufferView) =>\n      val asInt8Array = new Int8Array(buffer.buffer, buffer.byteOffset, buffer.byteLength)\n      val len = asInt8Array.length\n      val arrayBuffer = new Array[Byte](len)\n      insecureRandom.nextBytes(arrayBuffer)\n      var i = 0\n      while (i != len) {\n        asInt8Array(i) = arrayBuffer(i)\n        i += 1\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "web/src/test/scala/net/wiringbits/forms/ForgotPasswordFormDataSpec.scala",
    "content": "package net.wiringbits.forms\n\nimport net.wiringbits.common.models.{Captcha, Email}\nimport org.scalatest.matchers.must.Matchers.*\nimport org.scalatest.wordspec.AnyWordSpec\n\nclass ForgotPasswordFormDataSpec extends AnyWordSpec {\n\n  private val initialForm = ForgotPasswordFormData.initial(\n    emailLabel = \"Email\"\n  )\n\n  private val validForm = initialForm\n    .copy(\n      email = initialForm.email.updated(Email.validate(\"hello@test.com\")),\n      captcha = Some(Captcha.trusted(\"test\"))\n    )\n\n  private val allDataInvalidForm = initialForm\n    .copy(\n      email = initialForm.email.updated(Email.validate(\"x@\"))\n    )\n\n  \"fields\" should {\n    \"return the expected fields\" in {\n      val expected = List(\"email\")\n      initialForm.fields.map(_.name).toSet must be(expected.toSet)\n    }\n  }\n\n  \"formValidationErrors\" should {\n    \"return no errors when everything mandatory is correct\" in {\n      val result = validForm.formValidationErrors\n      result must be(empty)\n    }\n\n    \"return all errors\" in {\n      allDataInvalidForm.formValidationErrors.size must be(2)\n    }\n  }\n\n  \"submitRequest\" should {\n\n    \"return a request when the emmail is valid\" in {\n      val result = validForm.submitRequest\n      result.isDefined must be(true)\n    }\n\n    \"return None when the data is not valid\" in {\n      val form = validForm\n      val invalidEmail = form.copy(email = allDataInvalidForm.email)\n\n      List(invalidEmail).foreach { form =>\n        form.submitRequest.isDefined must be(false)\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "web/src/test/scala/net/wiringbits/forms/ResendVerifyEmailFormDataSpec.scala",
    "content": "package net.wiringbits.forms\n\nimport net.wiringbits.common.models.{Captcha, Email}\nimport org.scalatest.matchers.must.Matchers.*\nimport org.scalatest.wordspec.AnyWordSpec\n\nclass ResendVerifyEmailFormDataSpec extends AnyWordSpec {\n  private val initialForm = ResendVerifyEmailFormData.initial(\n    texts = ResendVerifyEmailFormData.Texts(\"Captcha message\"),\n    emailLabel = \"Email\"\n  )\n\n  private val validForm = initialForm\n    .copy(\n      email = initialForm.email.updated(Email.validate(\"hello@test.com\")),\n      captcha = Some(Captcha.trusted(\"test\"))\n    )\n\n  private val allDataInvalidForm = initialForm\n    .copy(\n      email = initialForm.email.updated(Email.validate(\"x@\")),\n      captcha = None\n    )\n\n  \"fields\" should {\n    \"return the expected fields\" in {\n      val expected = List(\"email\")\n      initialForm.fields.map(_.name).toSet must be(expected.toSet)\n    }\n  }\n\n  \"formValidationErrors\" should {\n    \"return no errors when everything mandatory is correct\" in {\n      val result = validForm.formValidationErrors\n      result must be(empty)\n    }\n\n    \"return all errors\" in {\n      allDataInvalidForm.formValidationErrors.size must be(2)\n    }\n  }\n\n  \"submitRequest\" should {\n\n    \"return a request when the emmail is valid\" in {\n      val result = validForm.submitRequest\n      result.isDefined must be(true)\n    }\n\n    \"return None when the data is not valid\" in {\n      val form = validForm\n      val invalidEmail = form.copy(email = allDataInvalidForm.email)\n\n      List(invalidEmail).foreach { form =>\n        form.submitRequest.isDefined must be(false)\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "web/src/test/scala/net/wiringbits/forms/ResetPasswordFormDataSpec.scala",
    "content": "package net.wiringbits.forms\n\nimport net.wiringbits.common.models.{Password, UserToken}\nimport org.scalatest.matchers.must.Matchers.{be, empty, must}\nimport org.scalatest.wordspec.AnyWordSpec\n\nimport java.util.UUID\n\nclass ResetPasswordFormDataSpec extends AnyWordSpec {\n  private val initialForm = ResetPasswordFormData.initial(\n    passwordLabel = \"Password\",\n    repeatPasswordLabel = \"Repeat password\",\n    token = Some(UserToken(UUID.randomUUID, UUID.randomUUID))\n  )\n\n  private val validForm = initialForm\n    .copy(\n      password = initialForm.password.updated(Password.validate(\"123456789\")),\n      repeatPassword = initialForm.repeatPassword.updated(Password.validate(\"123456789\")),\n      token = Some(UserToken(UUID.randomUUID, UUID.randomUUID))\n    )\n\n  private val allDataInvalidForm = initialForm\n    .copy(\n      password = initialForm.password.updated(Password.validate(\"x\")),\n      repeatPassword = initialForm.repeatPassword.updated(Password.validate(\"x\")),\n      token = None\n    )\n\n  \"fields\" should {\n    \"return the expected fields\" in {\n      val expected = List(\"password\", \"repeatPassword\")\n      initialForm.fields.map(_.name).toSet must be(expected.toSet)\n    }\n  }\n\n  \"formValidationErrors\" should {\n    \"return no errors when everything mandatory is correct\" in {\n      val result = validForm.formValidationErrors\n      result must be(empty)\n    }\n\n    \"return error when the password do not match\" in {\n      val form = validForm.copy(\n        password = initialForm.password.updated(Password.validate(\"123456789\")),\n        repeatPassword = initialForm.repeatPassword.updated(Password.validate(\"123456788\"))\n      )\n\n      form.formValidationErrors must be(List(\"The passwords does not match\"))\n    }\n\n    \"return no password match error when password isn't valid\" in {\n      val form = validForm.copy(\n        password = initialForm.password.updated(Password.validate(\"19\")),\n        repeatPassword = initialForm.repeatPassword.updated(Password.validate(\"123456789\"))\n      )\n\n      form.formValidationErrors.contains(\"The passwords does not match\") must be(false)\n    }\n\n    \"return no password match error when repeatPassword isn't valid\" in {\n      val form = validForm.copy(\n        password = initialForm.password.updated(Password.validate(\"123456789\")),\n        repeatPassword = initialForm.repeatPassword.updated(Password.validate(\"12\"))\n      )\n\n      form.formValidationErrors.contains(\"The passwords does not match\") must be(false)\n    }\n\n    \"return all errors\" in {\n      allDataInvalidForm.formValidationErrors.size must be(2)\n    }\n  }\n\n  \"submitRequest\" should {\n    \"return None when the data is not valid\" in {\n      val form = validForm\n      val invalidPassword = form.copy(password = allDataInvalidForm.password)\n\n      List(invalidPassword).foreach { form =>\n        form.submitRequest.isDefined must be(false)\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "web/src/test/scala/net/wiringbits/forms/SignInFormDataSpec.scala",
    "content": "package net.wiringbits.forms\n\nimport net.wiringbits.common.models.{Captcha, Email, Password}\nimport org.scalatest.matchers.must.Matchers.*\nimport org.scalatest.wordspec.AnyWordSpec\n\nclass SignInFormDataSpec extends AnyWordSpec {\n\n  private val initialForm = SignInFormData.initial(\n    emailLabel = \"Email\",\n    passwordLabel = \"Password\"\n  )\n\n  private val validForm = initialForm\n    .copy(\n      email = initialForm.email.updated(Email.validate(\"hello@test.com\")),\n      password = initialForm.password.updated(Password.validate(\"123456789\")),\n      captcha = Some(Captcha.trusted(\"test\"))\n    )\n\n  private val allDataInvalidForm = initialForm\n    .copy(\n      email = initialForm.email.updated(Email.validate(\"x@\")),\n      password = initialForm.password.updated(Password.validate(\"x\")),\n      captcha = None\n    )\n\n  \"fields\" should {\n    \"return the expected fields\" in {\n      val expected = List(\"email\", \"password\")\n      initialForm.fields.map(_.name).toSet must be(expected.toSet)\n    }\n  }\n\n  \"formValidationErrors\" should {\n    \"return no errors when everything mandatory is correct\" in {\n      val result = validForm.formValidationErrors\n      result must be(empty)\n    }\n\n    \"return all errors\" in {\n      allDataInvalidForm.formValidationErrors.size must be(2)\n    }\n  }\n\n  \"submitRequest\" should {\n\n    \"return a request when the data is valid\" in {\n      val result = validForm.submitRequest\n      result.isDefined must be(true)\n    }\n\n    \"return None when the data is not valid\" in {\n      val form = validForm\n      val invalidEmail = form.copy(email = allDataInvalidForm.email)\n      val invalidPassword = form.copy(password = allDataInvalidForm.password)\n\n      List(invalidEmail, invalidPassword).foreach { form =>\n        form.submitRequest.isDefined must be(false)\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "web/src/test/scala/net/wiringbits/forms/SignUpFormDataSpec.scala",
    "content": "package net.wiringbits.forms\n\nimport net.wiringbits.common.models.{Captcha, Email, Name, Password}\nimport org.scalatest.matchers.must.Matchers.*\nimport org.scalatest.wordspec.AnyWordSpec\n\nclass SignUpFormDataSpec extends AnyWordSpec {\n\n  private val initialForm = SignUpFormData.initial(\n    nameLabel = \"name\",\n    emailLabel = \"Email\",\n    passwordLabel = \"Password\",\n    repeatPasswordLabel = \"Repeat password\"\n  )\n\n  private val validForm = initialForm\n    .copy(\n      name = initialForm.name.updated(Name.validate(\"someone\")),\n      email = initialForm.email.updated(Email.validate(\"hello@test.com\")),\n      password = initialForm.password.updated(Password.validate(\"123456789\")),\n      repeatPassword = initialForm.repeatPassword.updated(Password.validate(\"123456789\")),\n      captcha = Some(Captcha.trusted(\"test\"))\n    )\n\n  private val allDataInvalidForm = initialForm\n    .copy(\n      name = initialForm.name.updated(Name.validate(\"x\")),\n      email = initialForm.email.updated(Email.validate(\"x@\")),\n      password = initialForm.password.updated(Password.validate(\"x\")),\n      repeatPassword = initialForm.repeatPassword.updated(Password.validate(\"x\")),\n      captcha = None\n    )\n\n  \"fields\" should {\n    \"return the expected fields\" in {\n      val expected = List(\"name\", \"email\", \"password\", \"repeatPassword\")\n      initialForm.fields.map(_.name).toSet must be(expected.toSet)\n    }\n  }\n\n  \"formValidationErrors\" should {\n    \"return no errors when everything mandatory is correct\" in {\n      val result = validForm.formValidationErrors\n      result must be(empty)\n    }\n\n    \"return error when the password do not match\" in {\n      val form = validForm.copy(\n        password = initialForm.password.updated(Password.validate(\"123456789\")),\n        repeatPassword = initialForm.repeatPassword.updated(Password.validate(\"123456788\"))\n      )\n\n      form.formValidationErrors must be(List(\"The passwords does not match\"))\n    }\n\n    \"return no password match error when password isn't valid\" in {\n      val form = validForm.copy(\n        password = initialForm.password.updated(Password.validate(\"19\")),\n        repeatPassword = initialForm.repeatPassword.updated(Password.validate(\"123456789\"))\n      )\n\n      form.formValidationErrors.contains(\"The passwords does not match\") must be(false)\n    }\n\n    \"return no password match error when repeatPassword isn't valid\" in {\n      val form = validForm.copy(\n        password = initialForm.password.updated(Password.validate(\"123456789\")),\n        repeatPassword = initialForm.repeatPassword.updated(Password.validate(\"12\"))\n      )\n\n      form.formValidationErrors.contains(\"The passwords does not match\") must be(false)\n    }\n\n    \"return all errors\" in {\n      allDataInvalidForm.formValidationErrors.size must be(2)\n    }\n  }\n\n  \"submitRequest\" should {\n\n    \"return a request when the data is valid\" in {\n      val result = validForm.submitRequest\n      result.isDefined must be(true)\n    }\n\n    \"return None when the data is not valid\" in {\n      val form = validForm\n      val invalidName = form.copy(name = allDataInvalidForm.name)\n      val invalidEmail = form.copy(email = allDataInvalidForm.email)\n      val invalidPassword = form.copy(password = allDataInvalidForm.password)\n\n      List(invalidName, invalidEmail, invalidPassword).foreach { form =>\n        form.submitRequest.isDefined must be(false)\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "web/src/test/scala/net/wiringbits/forms/UpdateInfoFormDataSpec.scala",
    "content": "package net.wiringbits.forms\n\nimport net.wiringbits.common.models.{Email, Name}\nimport org.scalatest.matchers.must.Matchers.*\nimport org.scalatest.wordspec.AnyWordSpec\n\nclass UpdateInfoFormDataSpec extends AnyWordSpec {\n\n  private val initialForm = UpdateInfoFormData.initial(\n    nameLabel = \"name\",\n    emailLabel = \"Email\"\n  )\n\n  private val validForm = initialForm\n    .copy(\n      name = initialForm.name.updated(Name.validate(\"someone\")),\n      email = initialForm.email.updated(Email.validate(\"hello@test.com\"))\n    )\n\n  private val allDataInvalidForm = initialForm\n    .copy(\n      name = initialForm.name.updated(Name.validate(\"x\")),\n      email = initialForm.email.updated(Email.validate(\"x@\"))\n    )\n\n  \"fields\" should {\n    \"return the expected fields\" in {\n      val expected = List(\"name\")\n      initialForm.fields.map(_.name).toSet must be(expected.toSet)\n    }\n  }\n\n  \"formValidationErrors\" should {\n    \"return no errors when everything mandatory is correct\" in {\n      val result = validForm.formValidationErrors\n      result must be(empty)\n    }\n\n    \"return all errors\" in {\n      allDataInvalidForm.formValidationErrors.size must be(1)\n    }\n  }\n\n  \"submitRequest\" should {\n\n    \"return a request when the data is valid\" in {\n      val result = validForm.submitRequest\n      result.isDefined must be(true)\n    }\n\n    \"return None when the data is not valid\" in {\n      val result = allDataInvalidForm.submitRequest\n      result.isDefined must be(false)\n    }\n  }\n}\n"
  },
  {
    "path": "web/src/test/scala/net/wiringbits/forms/UpdatePasswordFormDataSpec.scala",
    "content": "package net.wiringbits.forms\n\nimport net.wiringbits.common.models.Password\nimport org.scalatest.matchers.must.Matchers.{be, empty, must}\nimport org.scalatest.wordspec.AnyWordSpec\n\nclass UpdatePasswordFormDataSpec extends AnyWordSpec {\n\n  private val initialForm = UpdatePasswordFormData.initial(\n    oldPasswordLabel = \"Old password\",\n    passwordLabel = \"Password\",\n    repeatPasswordLabel = \"Repeat password\"\n  )\n\n  private val validForm = initialForm\n    .copy(\n      oldPassword = initialForm.oldPassword.updated(Password.validate(\"1234567890\")),\n      password = initialForm.password.updated(Password.validate(\"123456789\")),\n      repeatPassword = initialForm.repeatPassword.updated(Password.validate(\"123456789\"))\n    )\n\n  private val allDataInvalidForm = initialForm\n    .copy(\n      oldPassword = initialForm.oldPassword.updated(Password.validate(\"x\")),\n      password = initialForm.password.updated(Password.validate(\"x\")),\n      repeatPassword = initialForm.repeatPassword.updated(Password.validate(\"x\"))\n    )\n\n  \"fields\" should {\n    \"return the expected fields\" in {\n      val expected = List(\"oldPassword\", \"password\", \"repeatPassword\")\n      initialForm.fields.map(_.name).toSet must be(expected.toSet)\n    }\n  }\n\n  \"formValidationErrors\" should {\n    \"return no errors when everything mandatory is correct\" in {\n      val result = validForm.formValidationErrors\n      result must be(empty)\n    }\n\n    \"return error when the password do not match\" in {\n      val form = validForm.copy(\n        password = initialForm.password.updated(Password.validate(\"123456789\")),\n        repeatPassword = initialForm.repeatPassword.updated(Password.validate(\"123456788\"))\n      )\n\n      form.formValidationErrors must be(List(\"The passwords does not match\"))\n    }\n\n    \"return no password match error when password isn't valid\" in {\n      val form = validForm.copy(\n        password = initialForm.password.updated(Password.validate(\"19\")),\n        repeatPassword = initialForm.repeatPassword.updated(Password.validate(\"123456789\"))\n      )\n\n      form.formValidationErrors.contains(\"The passwords does not match\") must be(false)\n    }\n\n    \"return no password match error when repeatPassword isn't valid\" in {\n      val form = validForm.copy(\n        password = initialForm.password.updated(Password.validate(\"123456789\")),\n        repeatPassword = initialForm.repeatPassword.updated(Password.validate(\"12\"))\n      )\n\n      form.formValidationErrors.contains(\"The passwords does not match\") must be(false)\n    }\n\n    \"return all errors\" in {\n      allDataInvalidForm.formValidationErrors.size must be(1)\n    }\n  }\n\n  \"submitRequest\" should {\n\n    \"return a request when the data is valid\" in {\n      val result = validForm.submitRequest\n      result.isDefined must be(true)\n    }\n\n    \"return None when the data is not valid\" in {\n      val form = validForm\n      val invalidOldPassword = form.copy(oldPassword = allDataInvalidForm.oldPassword)\n      val invalidPassword = form.copy(password = allDataInvalidForm.password)\n\n      List(invalidOldPassword, invalidPassword).foreach { form =>\n        form.submitRequest.isDefined must be(false)\n      }\n    }\n  }\n}\n"
  }
]