master 8089a40f739d cached
247 files
401.7 KB
104.3k tokens
11 symbols
1 requests
Download .txt
Showing preview only (472K chars total). Download the full file or copy to clipboard to get everything.
Repository: wiringbits/scala-webapp-template
Branch: master
Commit: 8089a40f739d
Files: 247
Total size: 401.7 KB

Directory structure:
gitextract_jae30_9x/

├── .github/
│   └── workflows/
│       ├── codepreview.yml
│       └── pull_request.yml
├── .gitignore
├── .nvmrc
├── .sbtopts
├── .scalafmt.conf
├── .sdkmanrc
├── LICENSE
├── README.md
├── build.sbt
├── custom.webpack.config.js
├── docs/
│   ├── README.md
│   ├── architecture.md
│   ├── design-decisions.md
│   ├── diagram-sources/
│   │   ├── architecture-infra.puml
│   │   ├── architecture-modules.puml
│   │   ├── architecture-server-actions.puml
│   │   ├── architecture-server-controllers.puml
│   │   ├── architecture-server-daos.puml
│   │   ├── architecture-server-external-apis.puml
│   │   ├── architecture-server-repositories.puml
│   │   ├── architecture-server-services.puml
│   │   └── architecture-server.puml
│   ├── learning-material.md
│   ├── setup-dev-environment.md
│   └── swagger-integration.md
├── infra/
│   ├── .gitignore
│   ├── README.md
│   ├── admin.yml
│   ├── config/
│   │   ├── nginx/
│   │   │   ├── admin-app-htpasswd
│   │   │   ├── admin_app_site.j2
│   │   │   ├── mime.types
│   │   │   ├── nginx.conf
│   │   │   ├── preview_admin_app_site.j2
│   │   │   ├── preview_web_app_site.j2
│   │   │   └── web_app_site.j2
│   │   └── server/
│   │       ├── dev.env.j2
│   │       └── server.service.j2
│   ├── demo-hosts.ini
│   ├── nginx.yml
│   ├── nginx_site_admin.yml
│   ├── nginx_site_web.yml
│   ├── preview_nginx_site_admin.yml
│   ├── preview_nginx_site_web.yml
│   ├── scripts/
│   │   ├── build-admin.sh
│   │   ├── build-server.sh
│   │   └── build-web.sh
│   ├── server.yml
│   ├── setup-postgres.md
│   ├── test-hosts.ini
│   └── web.yml
├── lib/
│   ├── api/
│   │   └── shared/
│   │       └── src/
│   │           └── main/
│   │               └── scala/
│   │                   └── net/
│   │                       └── wiringbits/
│   │                           └── api/
│   │                               ├── ApiClient.scala
│   │                               ├── endpoints/
│   │                               │   ├── AdminEndpoints.scala
│   │                               │   ├── AuthEndpoints.scala
│   │                               │   ├── EnvironmentConfigEndpoints.scala
│   │                               │   ├── HealthEndpoints.scala
│   │                               │   ├── UsersEndpoints.scala
│   │                               │   └── package.scala
│   │                               ├── models/
│   │                               │   ├── PlayErrorResponse.scala
│   │                               │   ├── admin/
│   │                               │   │   ├── AdminGetUserLogs.scala
│   │                               │   │   └── AdminGetUsers.scala
│   │                               │   ├── auth/
│   │                               │   │   ├── GetCurrentUser.scala
│   │                               │   │   ├── Login.scala
│   │                               │   │   └── Logout.scala
│   │                               │   ├── environmentconfig/
│   │                               │   │   └── GetEnvironmentConfig.scala
│   │                               │   ├── package.scala
│   │                               │   └── users/
│   │                               │       ├── CreateUser.scala
│   │                               │       ├── ForgotPassword.scala
│   │                               │       ├── GetUserLogs.scala
│   │                               │       ├── ResetPassword.scala
│   │                               │       ├── SendEmailVerificationToken.scala
│   │                               │       ├── UpdatePassword.scala
│   │                               │       ├── UpdateUser.scala
│   │                               │       └── VerifyEmail.scala
│   │                               └── utils/
│   │                                   └── Formatter.scala
│   ├── common/
│   │   ├── js/
│   │   │   └── src/
│   │   │       └── test/
│   │   │           └── scala/
│   │   │               └── java/
│   │   │                   └── security/
│   │   │                       └── SecureRandom.scala
│   │   └── shared/
│   │       └── src/
│   │           ├── main/
│   │           │   └── scala/
│   │           │       └── net/
│   │           │           └── wiringbits/
│   │           │               └── common/
│   │           │                   ├── ErrorMessages.scala
│   │           │                   └── models/
│   │           │                       ├── Captcha.scala
│   │           │                       ├── Email.scala
│   │           │                       ├── Name.scala
│   │           │                       ├── Password.scala
│   │           │                       └── UserToken.scala
│   │           └── test/
│   │               └── scala/
│   │                   └── net/
│   │                       └── wiringbits/
│   │                           └── common/
│   │                               └── models/
│   │                                   ├── EmailSpec.scala
│   │                                   ├── NameSpec.scala
│   │                                   ├── PasswordSpec.scala
│   │                                   └── UserTokenSpec.scala
│   └── ui/
│       └── src/
│           └── main/
│               └── scala/
│                   └── net/
│                       └── wiringbits/
│                           └── ui/
│                               └── components/
│                                   ├── core/
│                                   │   └── widgets/
│                                   │       └── ValidatedTextInput.scala
│                                   └── inputs/
│                                       └── inputs.scala
├── project/
│   ├── build.properties
│   └── plugins.sbt
├── server/
│   └── src/
│       ├── main/
│       │   ├── resources/
│       │   │   ├── application.conf
│       │   │   ├── evolutions/
│       │   │   │   └── default/
│       │   │   │       ├── 1.sql
│       │   │   │       └── 2.sql
│       │   │   ├── logback.xml
│       │   │   ├── messages
│       │   │   └── routes
│       │   └── scala/
│       │       ├── PekkoStream.scala
│       │       ├── controllers/
│       │       │   ├── AdminController.scala
│       │       │   ├── ApiRouter.scala
│       │       │   ├── AuthController.scala
│       │       │   ├── EnvironmentConfigController.scala
│       │       │   ├── HealthController.scala
│       │       │   ├── UsersController.scala
│       │       │   └── package.scala
│       │       └── net/
│       │           └── wiringbits/
│       │               ├── actions/
│       │               │   ├── auth/
│       │               │   │   ├── GetUserAction.scala
│       │               │   │   └── LoginAction.scala
│       │               │   ├── environmentconfig/
│       │               │   │   └── GetEnvironmentConfigAction.scala
│       │               │   ├── internal/
│       │               │   │   └── StreamPendingBackgroundJobsForeverAction.scala
│       │               │   └── users/
│       │               │       ├── CreateUserAction.scala
│       │               │       ├── ForgotPasswordAction.scala
│       │               │       ├── GetUserLogsAction.scala
│       │               │       ├── ResetPasswordAction.scala
│       │               │       ├── SendEmailVerificationTokenAction.scala
│       │               │       ├── UpdatePasswordAction.scala
│       │               │       ├── UpdateUserAction.scala
│       │               │       └── VerifyUserEmailAction.scala
│       │               ├── apis/
│       │               │   ├── EmailApi.scala
│       │               │   ├── EmailApiAWSImpl.scala
│       │               │   ├── ReCaptchaApi.scala
│       │               │   └── models/
│       │               │       └── EmailRequest.scala
│       │               ├── config/
│       │               │   ├── AWSConfig.scala
│       │               │   ├── BackgroundJobsExecutorConfig.scala
│       │               │   ├── EmailConfig.scala
│       │               │   ├── ReCaptchaConfig.scala
│       │               │   ├── SwaggerConfig.scala
│       │               │   ├── UserTokensConfig.scala
│       │               │   └── WebAppConfig.scala
│       │               ├── executors/
│       │               │   └── DatabaseExecutionContext.scala
│       │               ├── models/
│       │               │   ├── AWSAccessKeyId.scala
│       │               │   ├── AWSSecretAccessKey.scala
│       │               │   ├── ReCaptchaSecret.scala
│       │               │   ├── ReCaptchaSiteKey.scala
│       │               │   ├── SecretValue.scala
│       │               │   └── jobs/
│       │               │       ├── BackgroundJobPayload.scala
│       │               │       ├── BackgroundJobStatus.scala
│       │               │       └── BackgroundJobType.scala
│       │               ├── modules/
│       │               │   ├── ApisModule.scala
│       │               │   ├── ClockModule.scala
│       │               │   ├── ConfigModule.scala
│       │               │   ├── ExecutorsModule.scala
│       │               │   └── TasksModule.scala
│       │               ├── repositories/
│       │               │   ├── BackgroundJobsRepository.scala
│       │               │   ├── UserLogsRepository.scala
│       │               │   ├── UserTokensRepository.scala
│       │               │   ├── UsersRepository.scala
│       │               │   ├── daos/
│       │               │   │   ├── BackgroundJobDAO.scala
│       │               │   │   ├── UserLogsDAO.scala
│       │               │   │   ├── UserTokensDAO.scala
│       │               │   │   ├── UsersDAO.scala
│       │               │   │   └── package.scala
│       │               │   └── models/
│       │               │       ├── BackgroundJobData.scala
│       │               │       ├── User.scala
│       │               │       ├── UserLog.scala
│       │               │       ├── UserToken.scala
│       │               │       └── UserTokenType.scala
│       │               ├── services/
│       │               │   └── AdminService.scala
│       │               ├── tasks/
│       │               │   └── BackgroundJobsExecutorTask.scala
│       │               ├── util/
│       │               │   ├── DelayGenerator.scala
│       │               │   ├── EmailMessage.scala
│       │               │   ├── EmailsHelper.scala
│       │               │   ├── StringUtils.scala
│       │               │   ├── TokenGenerator.scala
│       │               │   └── TokensHelper.scala
│       │               └── validations/
│       │                   ├── ValidateCaptcha.scala
│       │                   ├── ValidateEmailIsAvailable.scala
│       │                   ├── ValidateEmailIsRegistered.scala
│       │                   ├── ValidatePasswordMatches.scala
│       │                   ├── ValidateUserIsNotVerified.scala
│       │                   ├── ValidateUserToken.scala
│       │                   └── ValidateVerifiedUser.scala
│       └── test/
│           └── scala/
│               ├── controllers/
│               │   ├── AdminControllerSpec.scala
│               │   ├── AuthControllerSpec.scala
│               │   ├── EnvironmentConfigControllerSpec.scala
│               │   ├── UsersControllerSpec.scala
│               │   └── common/
│               │       ├── PlayAPISpec.scala
│               │       └── PlayPostgresSpec.scala
│               ├── net/
│               │   └── wiringbits/
│               │       ├── apis/
│               │       │   └── ReCaptchaApiSpec.scala
│               │       ├── core/
│               │       │   ├── PostgresSpec.scala
│               │       │   ├── RepositoryComponents.scala
│               │       │   └── RepositorySpec.scala
│               │       ├── repositories/
│               │       │   ├── BackgroundJobsRepositorySpec.scala
│               │       │   ├── UserLogsRepositorySpec.scala
│               │       │   ├── UserTokensRepositorySpec.scala
│               │       │   └── UsersRepositorySpec.scala
│               │       └── util/
│               │           ├── DelayGeneratorSpec.scala
│               │           └── TokensHelperSpec.scala
│               └── utils/
│                   ├── Executors.scala
│                   ├── LoginUtils.scala
│                   └── RepositoryUtils.scala
└── web/
    └── src/
        ├── main/
        │   ├── js/
        │   │   ├── index.css
        │   │   └── index.html
        │   └── scala/
        │       └── net/
        │           └── wiringbits/
        │               ├── API.scala
        │               ├── App.scala
        │               ├── AppContext.scala
        │               ├── AppRouter.scala
        │               ├── AppTheme.scala
        │               ├── I18nMessages.scala
        │               ├── Main.scala
        │               ├── components/
        │               │   ├── AppSplash.scala
        │               │   ├── pages/
        │               │   │   ├── AboutPage.scala
        │               │   │   ├── DashboardPage.scala
        │               │   │   ├── ForgotPasswordPage.scala
        │               │   │   ├── HomePage.scala
        │               │   │   ├── ResendVerifyEmailPage.scala
        │               │   │   ├── ResetPasswordPage.scala
        │               │   │   ├── SignInPage.scala
        │               │   │   ├── SignUpPage.scala
        │               │   │   ├── UserEditPage.scala
        │               │   │   ├── VerifyEmailPage.scala
        │               │   │   └── VerifyEmailWithTokenPage.scala
        │               │   └── widgets/
        │               │       ├── AppBar.scala
        │               │       ├── AppCard.scala
        │               │       ├── EditPasswordForm.scala
        │               │       ├── EditUserForm.scala
        │               │       ├── Footer.scala
        │               │       ├── ForgotPasswordForm.scala
        │               │       ├── Loader.scala
        │               │       ├── LogList.scala
        │               │       ├── Logs.scala
        │               │       ├── ReCaptcha.scala
        │               │       ├── ResendVerifyEmailForm.scala
        │               │       ├── ResetPasswordForm.scala
        │               │       ├── SignInForm.scala
        │               │       ├── SignUpForm.scala
        │               │       └── UserInfo.scala
        │               ├── core/
        │               │   ├── I18nHooks.scala
        │               │   ├── I18nLang.scala
        │               │   └── ReactiveHooks.scala
        │               ├── forms/
        │               │   ├── ForgotPasswordFormData.scala
        │               │   ├── ResendVerifyEmailFormData.scala
        │               │   ├── ResetPasswordFormData.scala
        │               │   ├── SignInFormData.scala
        │               │   ├── SignUpFormData.scala
        │               │   ├── UpdateInfoFormData.scala
        │               │   └── UpdatePasswordFormData.scala
        │               ├── models/
        │               │   ├── AuthState.scala
        │               │   ├── User.scala
        │               │   └── UserMenuOption.scala
        │               └── services/
        │                   └── StorageService.scala
        └── test/
            └── scala/
                ├── java/
                │   └── security/
                │       └── SecureRandom.scala
                └── net/
                    └── wiringbits/
                        └── forms/
                            ├── ForgotPasswordFormDataSpec.scala
                            ├── ResendVerifyEmailFormDataSpec.scala
                            ├── ResetPasswordFormDataSpec.scala
                            ├── SignInFormDataSpec.scala
                            ├── SignUpFormDataSpec.scala
                            ├── UpdateInfoFormDataSpec.scala
                            └── UpdatePasswordFormDataSpec.scala

================================================
FILE CONTENTS
================================================

================================================
FILE: .github/workflows/codepreview.yml
================================================
name: Create preview environment

on:
  pull_request:
    branches: [ master ]
  push:
    branches: [ master, scala3 ]

concurrency:
  # The preview script can't handle concurrent deploys
  group: codepreview
  cancel-in-progress: false

# TODO: Define minimal permissions, I haven't found which one is necessary to allow writing comments on commits
# see https://docs.github.com/en/actions/using-jobs/assigning-permissions-to-jobs
#permissions:
#  contents: read # for checkout

jobs:
  preview:

    runs-on: ubuntu-latest

    steps:
      - name: Install ansible
        run: python3 -m pip install --user ansible

      - uses: coursier/cache-action@v6
      - uses: VirtusLab/scala-cli-setup@main

      - name: checkout
        uses: actions/checkout@v2

      - name: Setup Scala
        uses: japgolly/setup-everything-scala@v3.1
        with:
          java-version: 'adopt:1.11.0-11'
          node-version: '16.7.0'

      - name: Cache compiled code
        uses: actions/cache@v3
        with:
          path: |
            **/target/
            /home/runner/.ivy2/local
          key: compiled-code-preview-cache-${{ hashFiles('**/build.sbt') }}
          restore-keys: compiled-code-preview-cache-

      - name: Compile
        run: sbt compile

      - name: Create SSH key
        run: |
          mkdir -p ~/.ssh/
          echo "$CODEPREVIEW_PRIVATE_KEY" > ~/.ssh/id_rsa
          chmod 600 ~/.ssh/id_rsa
          echo "StrictHostKeyChecking=no" > ~/.ssh/config
        shell: bash
        env:
          CODEPREVIEW_PRIVATE_KEY: ${{ secrets.CODEPREVIEW_PRIVATE_KEY }}

      - name: Create codepreview scripts
        run: |
          rm -rf ./infra
          curl -u "github:$CODEPREVIEW_TOKEN" -O https://sssppa.wiringbits.dev/sssppa.zip
          unzip sssppa.zip -d .
          chmod +x ./infra/scripts/*.sh
        shell: bash
        env:
          CODEPREVIEW_TOKEN: ${{ secrets.CODEPREVIEW_TOKEN }}

      - name: Create preview env
        run: cd infra && ./scripts/deploy-preview.sh
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          PR_NUMBER: ${{ github.event.number }}



================================================
FILE: .github/workflows/pull_request.yml
================================================
name: Build the app

on:
  push:
    branches: [ master ]
  pull_request:
    branches: [ master ]

concurrency:
  # Only run once for latest commit per ref and cancel other (previous) runs.
  group: ci-${{ github.ref }}
  cancel-in-progress: true

permissions:
  contents: read # for checkout

jobs:
  build:

    runs-on: ubuntu-latest

    steps:
      - name: checkout
        uses: actions/checkout@v2

      - name: Setup Scala
        uses: japgolly/setup-everything-scala@v3.1
        with:
          java-version: 'adopt:1.11.0-11'
          node-version: '16.7.0'

      - name: Cache compiled code
        uses: actions/cache@v3
        with:
          path: |
            **/target/
            /home/runner/.ivy2/local
          key: compiled-code-cache-${{ hashFiles('**/build.sbt') }}
          restore-keys: compiled-code-cache-

      - name: Check code format
        run: sbt scalafmtCheckAll

      - name: Compile
        run: CI=true sbt compile

      - name: Run tests
        run: CI=true sbt test

      - name: Test summary
        if: always() # Always run, even if previous steps failed
        uses: test-summary/action@v2
        with:
          paths: "**/target/test-reports/*.xml"

      - name: Prepare web build
        run: sbt web/build

      - name: Prepare server build
        run: sbt server/dist


================================================
FILE: .gitignore
================================================
target/
.idea/
.bsp/
.vscode/
logs/
admin/build/
web/build/
local.env

# https://scalameta.org/metals/docs/editors/vscode/#files-and-directories-to-include-in-your-gitignore
.metals/
.bloop/
.ammonite/
metals.sbt


================================================
FILE: .nvmrc
================================================
16.7.0



================================================
FILE: .sbtopts
================================================
-J-Xmx4G
-J-XX:MaxMetaspaceSize=4G
-J-XX:+CMSClassUnloadingEnabled


================================================
FILE: .scalafmt.conf
================================================
version = 3.7.3
project.git = true
project.excludeFilters = [
]

runner.dialect=scala3

maxColumn = 120
assumeStandardLibraryStripMargin = false

continuationIndent.callSite = 2
continuationIndent.defnSite = 4

align.preset = none

onTestFailure = "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"


================================================
FILE: .sdkmanrc
================================================
java=11.0.16-tem


================================================
FILE: LICENSE
================================================
MIT License

Copyright (c) 2021 wiringbits

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.


================================================
FILE: README.md
================================================
# Wiringbits Web Application Template

![wiringbits](https://github.com/wiringbits/scala-webapp-template/workflows/Build%20the%20server%20app/badge.svg)
[![Scala.js](https://www.scala-js.org/assets/badges/scalajs-1.6.0.svg)](https://www.scala-js.org)

This 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.

If 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.


## Why?

Scala 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).

Our template provides all the necessary boilerplate to get started fast when building a traditional web application.

Don't waste your time evaluating every library required to build your web app, pick this template and go from there.

Using 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.


## Demo

We have a live demo so that you can get a taste on what our template provides.

- [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.
- [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.

### Short videos

Users app 1m demo:

[![Users app 1m demo](./docs/assets/demo-video-01.png)](https://youtu.be/hURUK4NCGBk "Users app 1m demo")

Deployment 2m demo:

[![Deployment 2m demo](./docs/assets/demo-video-02.png)](https://youtu.be/cN599dMa9EA "Deployment 2m demo")


## What's included?

1. User registration and authentication; Including email verification, profile updates, password recovery, and, captcha for spam prevention.
2. 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.
3. PostgreSQL as the data store layer, which is a reasonable choice for most web applications.
4. Practical components for testing your server-side code, writing tests for the Data/Api layer is real simple, no excuses accepted.
5. Practical frontend utilities, for example, test your frontend forms easily, consistent UI when performing asynchronous actions (fetching/submitting data), etc.
6. Typed data inputs, don't bother running simple validations to form data at the backend, accepted requests are already validated.
7. 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.
8. A simple to follow architecture, including short-guides for doing common tasks. 
9. Deployment scripts to cloud instances, we believe in simplicity and most projects are fine with simple managed servers instead of containers/K8s/etc.

## Get started

Read the [docs](./docs/README.md) or watch our [onboarding videos](http://onboarding.wiringbits.net).


## Presentations

There have been some presentations involing this project:

- 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)
- 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)

## Scala.js bundle size
These 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.

### Web app

![sssppa-web-code-size](./docs/assets/images/sssppa-web-code-size.png)

## Hire us

The 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.


================================================
FILE: build.sbt
================================================
import java.nio.file.Files
import java.nio.file.StandardCopyOption.REPLACE_EXISTING

ThisBuild / scalaVersion := "3.3.0"
ThisBuild / organization := "net.wiringbits"

val playJson = "3.0.1"
val sttp = "3.8.15"
val webappUtils = "0.7.2"
val anorm = "2.7.0"
val enumeratum = "1.7.2"
val scalaJavaTime = "2.5.0"
val tapir = "1.8.5"
val chimney = "0.8.0-RC1"

val consoleDisabledOptions = Seq("-Werror", "-Ywarn-unused", "-Ywarn-unused-import")

/** Say just `build` or `sbt build` to make a production bundle in `build`
  */
lazy val build = TaskKey[File]("build")

lazy val commonSettings: Project => Project = {
  _.settings(
    // Enable fatal warnings only when running in the CI
    scalacOptions ++= {
      sys.env
        .get("CI")
        .filter(_.nonEmpty)
        .map(_ => Seq("-Werror"))
        .getOrElse(Seq.empty[String])
    },
    Compile / compile / wartremoverErrors ++= List(
      Wart.ArrayEquals,
      //      Wart.Any,
      //      Wart.AsInstanceOf,
      //      Wart.ExplicitImplicitTypes,
      Wart.IsInstanceOf,
      Wart.JavaConversions,
      //      Wart.JavaSerializable,
      Wart.MutableDataStructures,
      //      Wart.NonUnitStatements,
      //      Wart.Nothing,
      Wart.Null,
      Wart.OptionPartial,
      //      Wart.Overloading,
      //      Wart.Product,
      //      Wart.PublicInference,
      Wart.Return,
      //      Wart.Serializable,
      //      Wart.StringPlusAny,
      //      Wart.ToString,
      Wart.TryPartial
    )
  )
}
// TODO: Reuse it in all projects
lazy val baseServerSettings: Project => Project = {
  _.settings(
    scalacOptions ++= Seq(
      "-unchecked",
      "-deprecation",
      "-feature"
    ),
    Compile / doc / scalacOptions ++= Seq("-no-link-warnings"),
    // Some options are very noisy when using the console and prevent us using it smoothly, let's disable them
    Compile / console / scalacOptions ~= (_.filterNot(consoleDisabledOptions.contains))
  )
}

// Used only by web projects
lazy val baseWebSettings: Project => Project =
  _.enablePlugins(ScalaJSPlugin)
    .settings(
      scalacOptions ++= Seq(
        "-deprecation", // Emit warning and location for usages of deprecated APIs.
        "-explaintypes", // Explain type errors in more detail.
        "-feature", // Emit warning and location for usages of features that should be imported explicitly.
        "-unchecked" // Enable additional warnings where generated code depends on assumptions.
      ),
      scalaJSUseMainModuleInitializer := true,
      /* disabled because it somehow triggers many warnings */
      scalaJSLinkerConfig := scalaJSLinkerConfig.value.withSourceMap(false),
      /* for slinky */
      libraryDependencies ++= Seq("me.shadaj" %%% "slinky-hot" % "0.7.3"),
      libraryDependencies ++= Seq(
        "io.github.cquiroz" %%% "scala-java-time" % scalaJavaTime,
        "io.github.cquiroz" %%% "scala-java-time-tzdb" % scalaJavaTime
      ),
      Test / fork := false, // sjs needs this to run tests
      Test / requireJsDomEnv := true
    )

// Used only by the lib projects
// TODO: This should go to commonSettings instead
lazy val baseLibSettings: Project => Project = _.settings(
  scalacOptions ++= Seq(
    "-deprecation", // Emit warning and location for usages of deprecated APIs.
    "-encoding",
    "utf-8", // Specify character encoding used by source files.
    "-explaintypes", // Explain type errors in more detail.
    "-feature", // Emit warning and location for usages of features that should be imported explicitly.
    "-unchecked" // Enable additional warnings where generated code depends on assumptions.
  )
)

/** Implement the `build` task define above. Most of this is really just to copy the index.html file around.
  */
lazy val browserProject: Project => Project =
  _.settings(
    build := {
      val artifacts = (Compile / fullOptJS / webpack).value
      val artifactFolder = (Compile / fullOptJS / crossTarget).value
      val jsFolder = baseDirectory.value / "src" / "main" / "js"
      val distFolder = baseDirectory.value / "build"

      distFolder.mkdirs()
      artifacts.foreach { artifact =>
        val target = artifact.data.relativeTo(artifactFolder) match {
          case None => distFolder / artifact.data.name
          case Some(relFile) => distFolder / relFile.toString
        }

        Files.copy(artifact.data.toPath, target.toPath, REPLACE_EXISTING)
      }

      // copy public resources
      Files
        .walk(jsFolder.toPath)
        .filter(x => !Files.isDirectory(x))
        .forEach(source => {
          source.toFile.relativeTo(jsFolder).foreach { relativeSource =>
            val dest = distFolder / relativeSource.toString
            dest.getParentFile.mkdirs()
            Files.copy(source, dest.toPath, REPLACE_EXISTING)
          }
        })

      // link the proper js bundle
      val indexFrom = baseDirectory.value / "src/main/js/index.html"
      val indexTo = distFolder / "index.html"

      val indexPatchedContent = {
        import collection.JavaConverters._
        Files
          .readAllLines(indexFrom.toPath, IO.utf8)
          .asScala
          .map(_.replaceAllLiterally("-fastopt", "-opt"))
          .mkString("\n")
      }

      Files.write(indexTo.toPath, indexPatchedContent.getBytes(IO.utf8))
      distFolder
    }
  )

// specify versions for all of reacts dependencies
lazy val reactNpmDeps: Project => Project =
  _.settings(
    stTypescriptVersion := "3.9.3",
    stIgnore += "react-proxy",
    Compile / npmDependencies ++= Seq(
      "react" -> "18.2.0",
      "@types/react" -> "18.0.33",
      "react-dom" -> "18.2.0",
      "@types/react-dom" -> "18.0.11",
      "csstype" -> "2.6.11",
      "react-proxy" -> "1.1.8",
      "@types/prop-types" -> "15.7.3"
    )
  )

lazy val withCssLoading: Project => Project =
  _.settings(
    /* custom webpack file to include css */
    Compile / webpackConfigFile := Some((ThisBuild / baseDirectory).value / "custom.webpack.config.js"),
    Test / webpackConfigFile := None, // it is important to avoid the custom webpack config in tests to get them passing
    Compile / npmDevDependencies ++= Seq(
      "webpack-merge" -> "4.2.2",
      "css-loader" -> "3.4.2",
      "style-loader" -> "1.1.3",
      "file-loader" -> "5.1.0",
      "url-loader" -> "3.0.0"
    )
  )

lazy val bundlerSettings: Project => Project =
  _.settings(
    Compile / fastOptJS / webpackExtraArgs += "--mode=development",
    Compile / fullOptJS / webpackExtraArgs += "--mode=production",
    Compile / fastOptJS / webpackDevServerExtraArgs += "--mode=development",
    Compile / fullOptJS / webpackDevServerExtraArgs += "--mode=production"
  )

// Used only by play-based projects
lazy val playSettings: Project => Project = {
  _.enablePlugins(PlayScala)
    .disablePlugins(PlayLayoutPlugin)
    .settings(
      // docs are huge and unnecessary
      Compile / doc / sources := Nil,
      Compile / doc / scalacOptions ++= Seq(
        "-no-link-warnings"
      ),
      // remove play noisy warnings
      play.sbt.routes.RoutesKeys.routesImport := Seq.empty,
      libraryDependencies ++= Seq(
        guice,
        evolutions,
        jdbc,
        ws,
        "com.google.inject" % "guice" % "5.1.0"
      ),
      // test
      libraryDependencies ++= Seq(
        "org.scalatestplus.play" %% "scalatestplus-play" % "6.0.0-M6" % Test,
        "org.scalatestplus" %% "mockito-4-6" % "3.2.15.0" % Test
      )
    )
}

lazy val common = (crossProject(JSPlatform, JVMPlatform) in file("lib/common"))
  .configure(baseLibSettings, commonSettings)
  .jsConfigure(_.enablePlugins(ScalaJSPlugin, ScalaJSBundlerPlugin, ScalablyTypedConverterPlugin))
  .settings(
    libraryDependencies ++= Seq()
  )
  .jvmSettings(
    libraryDependencies ++= Seq(
      "org.playframework" %% "play-json" % playJson,
      "net.wiringbits" %% "webapp-common" % webappUtils,
      "org.scalatest" %% "scalatest" % "3.2.16" % Test
    )
  )
  .jsSettings(
    useYarn := true,
    Test / fork := false, // sjs needs this to run tests
    stUseScalaJsDom := true,
    Compile / stMinimize := Selection.All,
    libraryDependencies ++= Seq(
      "io.github.cquiroz" %%% "scala-java-time" % scalaJavaTime,
      "org.playframework" %%% "play-json" % playJson,
      "net.wiringbits" %%% "webapp-common" % webappUtils,
      "org.scalatest" %%% "scalatest" % "3.2.16" % Test,
      "com.beachape" %%% "enumeratum" % enumeratum
    )
  )

// shared apis
lazy val api = (crossProject(JSPlatform, JVMPlatform) in file("lib/api"))
  .dependsOn(common)
  .configure(baseLibSettings, commonSettings)
  .jsConfigure(_.enablePlugins(ScalaJSPlugin, ScalaJSBundlerPlugin, ScalablyTypedConverterPlugin))
  .jvmSettings(
    libraryDependencies ++= Seq(
      "org.playframework" %% "play-json" % playJson,
      "com.softwaremill.sttp.client3" %% "core" % sttp,
      "com.softwaremill.sttp.tapir" %% "tapir-json-play" % tapir,
      "com.softwaremill.sttp.tapir" %% "tapir-sttp-client" % tapir
    )
  )
  .jsSettings(
    useYarn := true,
    Test / fork := false, // sjs needs this to run tests
    stUseScalaJsDom := true,
    Compile / stMinimize := Selection.All,
    libraryDependencies ++= Seq(
      "org.playframework" %%% "play-json" % playJson,
      "org.scalatest" %%% "scalatest" % "3.2.16" % Test,
      "com.beachape" %%% "enumeratum" % enumeratum,
      "com.softwaremill.sttp.client3" %%% "core" % sttp,
      "com.softwaremill.sttp.tapir" %%% "tapir-json-play" % tapir,
      "com.softwaremill.sttp.tapir" %%% "tapir-sttp-client" % tapir
    )
  )

// shared on the ui only
lazy val ui = (project in file("lib/ui"))
  .configure(baseLibSettings, commonSettings)
  .configure(_.enablePlugins(ScalaJSPlugin, ScalaJSBundlerPlugin, ScalablyTypedConverterPlugin))
  .dependsOn(api.js, common.js)
  .settings(
    name := "wiringbits-lib-ui",
    useYarn := true,
    Test / requireJsDomEnv := true,
    Test / fork := false, // sjs needs this to run tests
    stTypescriptVersion := "3.9.3",
    // material-ui is provided by a pre-packaged library
    stIgnore ++= List(
      "@mui/material",
      "@mui/icons-material",
      "@mui/joy",
      "@emotion/react",
      "@emotion/styled",
      "react-router",
      "react-router-dom"
    ),
    Compile / npmDependencies ++= Seq(
      "@mui/material" -> "5.11.16",
      "@mui/icons-material" -> "5.11.16",
      "@mui/joy" -> "5.0.0-alpha.74",
      "@emotion/react" -> "11.10.6",
      "@emotion/styled" -> "11.10.6",
      "react-router" -> "5.1.2",
      "react-router-dom" -> "5.1.2"
    ),
    stFlavour := Flavour.Slinky,
    stReactEnableTreeShaking := Selection.All,
    stUseScalaJsDom := true,
    Compile / stMinimize := Selection.All,
    libraryDependencies ++= Seq(
      "io.github.cquiroz" %%% "scala-java-time" % scalaJavaTime,
      "org.scala-js" %%% "scala-js-macrotask-executor" % "1.1.1",
      "com.olvind.st-material-ui" %%% "st-material-ui-icons-slinky" % "5.11.16",
      "net.wiringbits" %%% "slinky-utils" % webappUtils,
      "org.scalatest" %%% "scalatest" % "3.2.16" % Test,
      "com.beachape" %%% "enumeratum" % enumeratum
    )
  )

lazy val server = (project in file("server"))
  .dependsOn(common.jvm, api.jvm)
  .configure(baseServerSettings, commonSettings, playSettings)
  .settings(
    name := "wiringbits-server",
    fork := true,
    Test / fork := true, // allows for graceful shutdown of containers once the tests have finished running
    libraryDependencies ++= Seq(
      "org.playframework.anorm" %% "anorm" % anorm,
      "org.playframework.anorm" %% "anorm-postgres" % anorm,
      "org.playframework" %% "play-json" % playJson,
      "org.postgresql" % "postgresql" % "42.6.0",
      "de.svenkubiak" % "jBCrypt" % "0.4.3",
      "commons-validator" % "commons-validator" % "1.7",
      "com.dimafeng" %% "testcontainers-scala-scalatest" % "0.40.16" % "test",
      "com.dimafeng" %% "testcontainers-scala-postgresql" % "0.40.16" % "test",
      "com.softwaremill.sttp.client3" %% "core" % sttp % "test",
      "com.softwaremill.sttp.client3" %% "async-http-client-backend-future" % sttp % "test",
      // "net.wiringbits" %% "admin-data-explorer-play-server" % webappUtils,
      "software.amazon.awssdk" % "ses" % "2.17.141",
      "jakarta.xml.bind" % "jakarta.xml.bind-api" % "4.0.0",
      "org.apache.commons" % "commons-text" % "1.10.0",
      // JAX-B dependencies for JDK 9+, required to use play sessions
      "javax.xml.bind" % "jaxb-api" % "2.3.1",
      "javax.annotation" % "javax.annotation-api" % "1.3.2",
      "javax.el" % "javax.el-api" % "3.0.0",
      "org.glassfish" % "javax.el" % "3.0.0",
      "com.beachape" %% "enumeratum" % enumeratum,
      "io.scalaland" %% "chimney" % chimney,
      "com.softwaremill.sttp.tapir" %% "tapir-swagger-ui-bundle" % tapir,
      "com.softwaremill.sttp.tapir" %% "tapir-json-play" % tapir,
      "com.softwaremill.sttp.tapir" %% "tapir-play-server" % tapir,
      "org.apache.pekko" %% "pekko-stream" % "1.0.1"
    )
  )

lazy val webBuildInfoSettings: Project => Project = _.enablePlugins(BuildInfoPlugin)
  .settings(
    buildInfoKeys := Seq[BuildInfoKey](name, version, scalaVersion, sbtVersion),
    buildInfoKeys ++= {
      val apiUrl = sys.env.get("API_URL")
      val values = Seq(
        "apiUrl" -> apiUrl
      )
      // Logging these values is useful to make sure that the necessary settings
      // are being overriden when packaging the app.
      sLog.value.info(s"BuildInfo settings:\n${values.mkString("\n")}")
      values.map(t => BuildInfoKey(t._1, t._2))
    },
    buildInfoPackage := "net.wiringbits",
    buildInfoUsePackageAsPath := true
  )

lazy val web = (project in file("web"))
  .dependsOn(common.js, api.js, ui)
  .enablePlugins(ScalablyTypedConverterPlugin)
  .configure(
    baseWebSettings,
    browserProject,
    commonSettings,
    reactNpmDeps,
    withCssLoading,
    bundlerSettings,
    webBuildInfoSettings
  )
  .settings(
    name := "wiringbits-web",
    useYarn := true,
    webpackDevServerPort := 8080,
    stFlavour := Flavour.Slinky,
    stReactEnableTreeShaking := Selection.All,
    stUseScalaJsDom := true,
    Compile / stMinimize := Selection.All,
    // material-ui is provided by a pre-packaged library
    stIgnore ++= List("@mui/material", "@mui/icons-material", "@mui/joy", "react-router", "react-router-dom"),
    Compile / npmDependencies ++= Seq(
      "@mui/material" -> "5.11.16",
      "@mui/icons-material" -> "5.11.16",
      "@mui/joy" -> "5.0.0-alpha.74",
      "@emotion/styled" -> "11.10.6",
      "@emotion/react" -> "11.10.6",
      "react-router" -> "5.1.2",
      "react-router-dom" -> "5.1.2",
      "react-google-recaptcha" -> "2.1.0",
      "@types/react-google-recaptcha" -> "2.1.0"
    ),
    libraryDependencies ++= Seq(
      "org.playframework" %%% "play-json" % playJson,
      "com.softwaremill.sttp.client3" %%% "core" % sttp,
      "org.scala-js" %%% "scala-js-macrotask-executor" % "1.1.1",
      "com.olvind.st-material-ui" %%% "st-material-ui-icons-slinky" % "5.11.16",
      "io.monix" %%% "monix-reactive" % "3.4.1",
      "com.softwaremill.sttp.tapir" %%% "tapir-sttp-client" % tapir
    ),
    libraryDependencies ++= Seq(
      "org.scalatest" %%% "scalatest" % "3.2.16" % Test
    )
  )

lazy val root = (project in file("."))
  .aggregate(
    common.jvm,
    common.js,
    api.jvm,
    api.js,
    ui,
    server,
    web
  )
  .settings(
    publish := {},
    publishLocal := {}
  )

addCommandAlias("dev-web", ";web/fastOptJS::startWebpackDevServer;~web/fastOptJS")


================================================
FILE: custom.webpack.config.js
================================================
var path = require("path");
var merge = require('webpack-merge');
var generated = require('./scalajs.webpack.config');

var local = {
    devServer: {
        // the historyAPIFallback allows react-router to work
        historyApiFallback: true,
        proxy: {
            // when a request to /api is done, we want to apply a proxy
            '/api': {
                changeOrigin: true,
                cookieDomainRewrite: 'localhost',
                target: 'http://localhost:9000',
                pathRewrite: { '^/api': '/'},
                onProxyReq: (req) => {
                    if (req.getHeader('origin')) {
                        req.setHeader('origin', 'http://localhost:9000')
                    }
                }
            }
        }
    },
    resolve: {
        alias: {
            "js": path.resolve(__dirname, "../../../../src/main/js"),
        }
    },
    module: {
        rules: [
            {
                test: /\.css$/,
                use: ['style-loader', 'css-loader']
            },
            {
                test: /\.(ttf|eot|woff|png|jpg|glb|svg)$/,
                use: 'file-loader'
            },
            {
                test: /\.(eot)$/,
                use: 'url-loader'
            }
        ]
    }
}

module.exports = merge(generated, local);


================================================
FILE: docs/README.md
================================================
# Wiringbits Scala WebApp template - Docs

- [Setup development environment](./setup-dev-environment.md)
- [Architecture](./architecture.md)
- [Design desicions](./design-decisions.md)
- [Learning material](./learning-material.md)
- [Swagger integration](./swagger-integration.md).


## Diagrams

The docs include diagrams created with plantuml, you will need to compile them after they are updated.

Before you can compile those, you will require some dependencies:

1. `graphviz` which can be installed with `apt install graphviz`
2. Download `plantuml.jar` from the official [site](https://plantuml.com/starting).

Then, you can execute this to generate them all:
- `java -jar ~/Downloads/plantuml.jar ./diagram-sources -o ../assets/diagrams/`

Consider using the [plantuml plugin for IntelliJ](https://plugins.jetbrains.com/plugin/7017-plantuml-integration/) when editing diagrams, this way, you can preview the changes.


================================================
FILE: docs/architecture.md
================================================
# Architecture

The following diagrams illustrate the overall project architecture.

**Disclaimer** There are some parts of the code (specially `server`) that do not fit the architecture completely, we plan to get there.

## Infrastructure
![Infrastructure architecture diagram](./assets/diagrams/architecture-infra.png)

The [infra](../infra) project includes Ansible scripts to configure your own Server, be sure to check it out.

Summary:

- The app is usually hosted on a cloud server, let it be a DigitalOcean Droplet, an AWS EC2 instance, etc.
- Ubuntu 20.04 is the target OS, newer versions would likely work without issues.
- We recommend a managed database for Postgres.
- While the diagram shows all components in a single server, we can easily separate nginx as a load balancer + many server instances.
- Frontend apps are composed by static assets, stored at the server filesystem.
- `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.
- When the server API is being invoked, `nginx` will route the traffic to the server app.
- The server app connects to postgres and external services when necessary (like AWS SES).
- AWS SES is being used to send emails.


## Modules
![Modules architecture diagram](./assets/diagrams/architecture-modules.png)

The application modules make sure to share code when possible, some of them cross-compile to Scala.js:

- [LibCommon](../lib/common) has code shared over the whole application (Scala/Scala.js), it consists mostly of models.
- [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.
- [LibUI](../lib/ui) has code shared between the webapps (Scala.js), while it is mostly empty, it can include reusable components for the UI.
- [Web](../web) has the web app code for the regular user application (Scala.js).
- [Server](../server) has the code for the server app (Scala).


## Server
![Server architecture diagram](./assets/diagrams/architecture-server.png)

The server architecture is a mix taking advantage from Domain Driven Design (DDD), Hexagonal Architecture, and, Clean Architecture.

**NOTE** While current diagrams do not display it, there is are some core models that are shared on all layers.

Let's visit the layers from the top to the bottom.

### Controllers
![Controllers architecture diagram](./assets/diagrams/architecture-server-controllers.png)

Controllers is the entry point layer for the user requests, it is tied to the http-framework, it has these responsibilities:

- Decode requests into typed models.
- Authenticate requests.
- Delegate the work to the actions layer.
- Encode responses.

### Actions
![Actions architecture diagram](./assets/diagrams/architecture-server-actions.png)

Actions 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:

- Actions have 0 knowledge about anything on the `Controllers` layer.
- 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()`.
- 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.
- Checks authorization rules.
- Runs complex validations (like the ones requiring interactions with another layers, checking whether an email is already registered could be one example).
- Usually, an action would combine work from other layers, like reading data from a Repository and submitting it to an External API.


### Services
![Services architecture diagram](./assets/diagrams/architecture-server-services.png)

The Services layer is composed of business rules 

Combines the work from Repositories and ExternalApis

Expose functions handling business rules when they get complex enough to fit in Actions:

- Services have 0 knowledge about Actions/Controllers.
- Combines the work from Repositories and ExternalApis




### External APIs
![External APIs architecture diagram](./assets/diagrams/architecture-server-external-apis.png)

External APIs layer holds anything necessary to communicate with external services, for example, AWS SES.

When 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.

Of course, this layer won't know anything about Controllers/Actions/Services/Repositories.


### Repositories
![Repositories architecture diagram](./assets/diagrams/architecture-server-repositories.png)

Repositories 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.

Given 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.

Details:

- The work done by Repositories is mostly composing DAOs.
- Repositories do not know any other layer besides DAOs.
- This layer is **NOT** responsible of validating input format.
- This layer could validate whether an item already exists.

### DAOs
![DAOs architecture diagram](./assets/diagrams/architecture-server-daos.png)

The 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.

DAOs don't know about any other layer.


================================================
FILE: docs/design-decisions.md
================================================
# Design decisions
This document explains why we took certain design decisions on how the project is built/structured.

## 2022/Aug - Avoid default parameter in most cases
We 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.

Take this snippet as an example:

```scala
case class CreateUserApiRequest(name: String, age: Option[Int])

case class CreateUserData(name: String, yearsOld: Option[Int] = None)

def transform(request: CreateUserApiRequest): CreateUserData = request.into[CreateUserData].transform
```

While 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`).

Still, there can be exceptions:
- 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.


## 2022/Apr - Naming conventions for api/data models

The project follows some principles from DDD (Domain Driven Design), we use different models for different layers even if they look quite similar.

For 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:

```scala
import models._

def createUserApi(model: api.CreateUser)
def createUserData(model: data.CreateUser)
```

Unfortunately, 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.

Then, 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.


## 2022/Jan/23 - Avoid creating postgres extensions in evolution scripts

While 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.

In short, when creating a local database, you will see yourself creating extensions manually like `CREATE EXTENSION CITEXT;`

Ref: 0439d7b3159e01f886ceeb3f0ff0d2d471f5e304


## Undated - Do not use `Downs` in evolutions

From 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.


================================================
FILE: docs/diagram-sources/architecture-infra.puml
================================================
@startuml
Title Wiringbits Scala WebApp Template - Infrastructure Diagram

skinparam {
    ArrowColor Red
    linetype ortho
}

cloud Cloud {
    database Postgres
    rectangle UbuntuServer {
        node ScalaServerApp
        node nginx

        folder FileSystem {
            file WebAssets
            file AdminAssets
        }
    }

    component EmailService
    ScalaServerApp -> EmailService
    nginx -> WebAssets
    nginx --> AdminAssets
    nginx --> ScalaServerApp
    ScalaServerApp --> Postgres
}

person RegularUser
person AdminUser

RegularUser -> nginx
AdminUser --> nginx
@enduml

================================================
FILE: docs/diagram-sources/architecture-modules.puml
================================================
@startuml
Title Wiringbits Scala WebApp Template - Module Diagram

skinparam {
    ArrowColor Red
}

package LibCommon {
    [Typed models shared everywhere\n* Scala/Scala.js]
}

package LibUI {
    [Code shared on UI apps (web/admin)\n* Scala.js]
}

package LibAPI {
    [REST API client and models\n* Scala/Scala.js]
}

package WebApp {
    [The main web app\n* Scala.js]
}
package AdminApp {
    [The admin web app\n* Scala.js]
}
package ServerApp {
    [The server side app\n* Scala]
}

WebApp .left....> LibUI : uses
WebApp .left....> LibAPI : uses

AdminApp .right.> LibUI : uses
AdminApp .right.> LibAPI : uses

ServerApp .> LibAPI : uses

LibUI .up..> LibCommon : uses
LibAPI .down.> LibCommon : uses
@enduml


================================================
FILE: docs/diagram-sources/architecture-server-actions.puml
================================================
@startuml
Title Wiringbits Scala WebApp Template - Server Architecture - Actions

skinparam {
    linetype ortho
}

skinparam component {
  BackgroundColor LightBlue
}
skinparam rectangle {
  BackgroundColor White
}

rectangle Controllers {
    component Actions {
        rectangle Services {
            rectangle ExternalApis {
                rectangle ExternalApiClients {
                    rectangle ExternalApiModels
                }
            }
            rectangle Repositories {
                rectangle DAOs {
                    rectangle DataModels
                }
            }
        }
    }
}
@enduml


================================================
FILE: docs/diagram-sources/architecture-server-controllers.puml
================================================
@startuml
Title Wiringbits Scala WebApp Template - Server Architecture - Controllers

skinparam {
    linetype ortho
}

skinparam component {
  BackgroundColor LightBlue
}
skinparam rectangle {
  BackgroundColor White
}

component Controllers {
    rectangle Actions {
        rectangle Services {
            rectangle ExternalApis {
                rectangle ExternalApiClients {
                    rectangle ExternalApiModels
                }
            }
            rectangle Repositories {
                rectangle DAOs {
                    rectangle DataModels
                }
            }
        }
    }
}
@enduml


================================================
FILE: docs/diagram-sources/architecture-server-daos.puml
================================================
@startuml
Title Wiringbits Scala WebApp Template - Server Architecture - DAOs

skinparam {
    linetype ortho
}

skinparam component {
  BackgroundColor LightBlue
}
skinparam rectangle {
  BackgroundColor White
}

rectangle Controllers {
    rectangle Actions {
        rectangle Services {
            rectangle ExternalApis {
                rectangle ExternalApiClients {
                    rectangle ExternalApiModels
                }
            }
            rectangle Repositories {
                component DAOs {
                    rectangle DataModels
                }
            }
        }
    }
}
@enduml


================================================
FILE: docs/diagram-sources/architecture-server-external-apis.puml
================================================
@startuml
Title Wiringbits Scala WebApp Template - Server Architecture - External APIs

skinparam {
    linetype ortho
}

skinparam component {
  BackgroundColor LightBlue
}
skinparam rectangle {
  BackgroundColor White
}

rectangle Controllers {
    rectangle Actions {
        rectangle Services {
            component ExternalApis {
                rectangle ExternalApiClients {
                    rectangle ExternalApiModels
                }
            }
            rectangle Repositories {
                rectangle DAOs {
                    rectangle DataModels
                }
            }
        }
    }
}
@enduml


================================================
FILE: docs/diagram-sources/architecture-server-repositories.puml
================================================
@startuml
Title Wiringbits Scala WebApp Template - Server Architecture - Repositories

skinparam {
    linetype ortho
}

skinparam component {
  BackgroundColor LightBlue
}
skinparam rectangle {
  BackgroundColor White
}

rectangle Controllers {
    rectangle Actions {
        rectangle Services {
            rectangle ExternalApis {
                rectangle ExternalApiClients {
                    rectangle ExternalApiModels
                }
            }
            component Repositories {
                rectangle DAOs {
                    rectangle DataModels
                }
            }
        }
    }
}
@enduml


================================================
FILE: docs/diagram-sources/architecture-server-services.puml
================================================
@startuml
Title Wiringbits Scala WebApp Template - Server Architecture - Services

skinparam {
    linetype ortho
}

skinparam component {
  BackgroundColor LightBlue
}
skinparam rectangle {
  BackgroundColor White
}

rectangle Controllers {
    rectangle Actions {
        component Services {
            rectangle ExternalApis {
                rectangle ExternalApiClients {
                    rectangle ExternalApiModels
                }
            }
            rectangle Repositories {
                rectangle DAOs {
                    rectangle DataModels
                }
            }
        }
    }
}
@enduml


================================================
FILE: docs/diagram-sources/architecture-server.puml
================================================
@startuml
Title Wiringbits Scala WebApp Template - Server Architecture

skinparam {
    linetype ortho
}

rectangle Controllers {
    rectangle Actions {
        rectangle Services {
            rectangle ExternalApis {
                rectangle ExternalApiClients {
                    rectangle ExternalApiModels
                }
            }
            rectangle Repositories {
                rectangle DAOs {
                    rectangle DataModels
                }
            }
        }
    }
}
@enduml

================================================
FILE: docs/learning-material.md
================================================
# Learning material

These are the tools used by the template, you don't need to master them all but being familiar with them definitely helps:
- [Scala](https://scala-lang.org/), we use Scala 2.13 because it has great tooling support, we'll eventually upgrade to Scala 3.
- [Scala.js](https://www.scala-js.org/) powers the frontend side.
- [Scalablytyped](https://scalablytyped.org/) generates the Scala facades to interact with JavaScript libraries by converting TypeScript definitions to Scala.js facades.
- [yarn](https://yarnpkg.com) (v1) as the JavaScript package manager.
- [React](https://reactjs.org/) as the view library.
- [Slinky](https://slinky.dev/) being the Scala wrapper for React.
- [Webpack](https://webpack.js.org) to bundle the web apps.
- [Scalajs bundler](https://scalacenter.github.io/scalajs-bundler/) being the Scala wrapper for Webpack.
- [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).
- [Play Framework](https://playframework.com/) as the backend framework, used for the REST API.
- [sttp](https://github.com/softwaremill/sttp/) as the REST API client.
- [react-router](https://www.npmjs.com/package/react-router) is the frontend routing library.
- [play-json](https://github.com/playframework/play-json/) is the JSON library.
- [ansible](https://ansible.com/) as the tool for deploying everything to a VM.
- [nginx](https://nginx.org/en/) as the reverse proxy for handling the internet traffic, as well as the authentication mechanism for admin endpoints.
- [GitHub](https://github.com/features/actions) actions integration so that you have a way to get every commit tested.


================================================
FILE: docs/setup-dev-environment.md
================================================
# Setup development environment

Let's get started setting up your development environment.

**NOTE** The instructions will work better on Linux/Mac, there could be some details that do not work on Windows (help wanted).

There 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 

**[Table of Contents](http://tableofcontent.eu)**

- [Compile-time dependencies](compile-time-dependencies)
- [Runtime dependencies](#runtime-dependencies)
   - [Postgres](#postgres)
   - [AWS Email Service](#aws-email-service)
   - [direnv](#direnv)
   - [Custom config](#custom-config)
   - [Run](#run)
- [Test dependencies](#test-dependencies)
- [Deployment setup](#deployment-setup)

## Compile-time dependencies

1. Clone the repository

    ```shell
    git clone git@github.com:wiringbits/scala-webapp-template.git
    ```

2. 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:

   ```shell
   # sdkman_auto_env=true would pick the right jdk when moving into the project's directory
   $ cd scala-webapp-template
   
   Using java version 11.0.16-tem in this shell.
   
   # otherwise, you can set the jdk manually with `sdk env`
   $ sdk env
   
   Using java version 11.0.16-tem in this shell.
   
   # verify your version
   $ java -version
   openjdk version "11.0.16" 2022-07-19
   OpenJDK Runtime Environment Temurin-11.0.16+8 (build 11.0.16+8)
   OpenJDK 64-Bit Server VM Temurin-11.0.16+8 (build 11.0.16+8, mixed mode)
   ```

   **Hint**: [.sdkmanrc](../.sdkmanrc) defines our suggested jdk.

3. Install sbt, run `sdk install sbt` or follow the official [instructions](https://www.scala-sbt.org/download.html).

4. 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:

   ```shell
   # nvm can pick the right node version when moving into the project's directory
   $ cd scala-webapp-template
   Found '~/scala-webapp-template/.nvmrc' with version <16.7.0>
   Now using node v16.7.0 (npm v7.20.3)
   
   # otherwise, you can set the node version manually with `nvm use`
   $ nvm use
   Found '~/scala-webapp-template/.nvmrc' with version <16.7.0>
   Now using node v16.7.0 (npm v7.20.3)
   
   # verify your version
   $ node --version
   v16.7.0
   ```

   **Hint**: [.nvmrc](../.nvmrc) defines our suggested node version.

5. 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:

   ```shell
   # yarn -version
   1.22.11
   ```

That's it, now just run `sbt compile` to compile the project (the first time it could take several minutes).


## Runtime dependencies

### Postgres
PostgreSQL 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.

What 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).

**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.

Once you are connected into postgres, we'll create a database for our app, and, any necessary dependencies:

```postgres-sql
-- create a database for the app
CREATE DATABASE wiringbits_db;

-- connect to it
\c wiringbits_db;

-- create an extension used by the app
CREATE EXTENSION IF NOT EXISTS CITEXT;
```

### AWS Email Service
We 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.

This is an optional requirement, if you decide to ignore it, everything should work fine.

### direnv
[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.

In 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))

### Custom config
It 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).

There are two ways:

1. Update [application.conf](../server/src/main/resources/application.conf)
Update application.conf to set your environment specific values (just avoid committing these).
2. 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:

   ```shell
   # postgres settings
   export POSTGRES_HOST="127.0.0.1"
   export POSTGRES_DATABASE="wiringbits_db"
   export POSTGRES_USERNAME="postgres"
   export POSTGRES_PASSWORD="postgres"
   
   # emails
   export EMAIL_SENDER_ADDRESS="test@wiringbits.net"
   export EMAIL_PROVIDER=none

   # aws, required only if the email provider is AWS
   export AWS_REGION="us-west-2"
   export AWS_ACCESS_KEY_ID="REPLACE_ME"
   export AWS_SECRET_ACCESS_KEY="REPLACE_ME"
   ```

### Run

Time to run the app:

1. `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`.
2. `sbt dev-web` launches the main web app at `localhost:8080`


**Hints**:

- All these apps are automatically reloaded on code changes.
- The server app prints the settings after starting, double check that they match your custom settings.
- By default, outgoing emails are logged, be sure to check those to look for the email verification links.

## Test dependencies

Docker is the only required dependency to run the integration tests.

Each integration test mounts its own clean database through docker, which removes the need to worry about tests polluting data.

Check 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`

Commands:
1. `sbt test` runs all the tests.
2. `sbt server/test` runs the server tests only.

**Hint** IntelliJ allows running all tests in a file, or a single test through its UI, which is very handy.


## Deployment setup

Check the [infra](../infra/README.md) project.


================================================
FILE: docs/swagger-integration.md
================================================
# Swagger integration

We have a swagger integration so that users can explore the server API through swagger-ui.

Some highlights:

- 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.
- Swagger-ui is exposed locally at [http://localhost:9000/docs](http://localhost:9000/docs).
- Be sure to check existing [endpoints](../lib/api/shared/src/main/scala/net/wiringbits/api/endpoints) to see real examples.
- `Option[T]` values are supported, sending a json without the key and value will be interpreted as `None`, otherwise, `Some(value)` will be sent.

## Creating an endpoint definition
We have to define our endpoints at the [endpoints](../lib/api/shared/src/main/scala/net/wiringbits/api/endpoints) package, for example:

```scala
val basicPostEndpoint = endpoint
  .post("basic") // points to POST http://localhost:9000/basic
  .tag("Misc") // tags the endpoint as "Misc" on swagger-ui
  .in(
    jsonBody[Basic.Request].example( // expects a JSON body of type BasicGet.Request with example values
      BasicGet.Request(
        name = "Alexis",
        email = "alexis@wiringbits.net"
      )
    )
  )
  .out(
    jsonBody[Basic.Response].example( // returns a JSON body of type BasicGet.Response with example values
      BasicGet.Response(
        message = "Hello Alexis!"
      )
    )
  )
```

Api models must have an `implicit Schema` defined, for example:

```scala
Schema
  .derived[Response]
  .name(Schema.SName("BasicResponse"))
  .description("Says hello to the user")
```

And then integrate the endpoint to the [ApiRouter](../server/src/main/scala/controllers/ApiRouter.scala) file:

```scala
object ApiRouter {
  private def routes(implicit ec: ExecutionContext): List[AnyEndpoint] = List(
    basicPostEndpoint
  )
}
```

## Endpoint user authentication details

We 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.

Any 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
authHandler: ServerRequest => Future[UUID]`, for example:

[//]: # (TODO: change Future[UUID] to Future[UserId] after mergin typo)
```scala
def basicEndpoint(implicit authHandler: ServerRequest => Future[UUID]) = endpoint.get
  .in(userAuth)
```

For more information about creating endpoints, please check the [tapir documentation](https://tapir.softwaremill.com/en/latest/).


================================================
FILE: infra/.gitignore
================================================
apps/
.vault
prod-hosts.ini
config/server/demo.env.j2
.scala-build/



================================================
FILE: infra/README.md
================================================
# Infra
This project includes the necessary scripts and configuration files to deploy the applications to cloud servers (like a DigitalOcean Droplet, or an Amazon EC2 instance).

## Requirements
The scripts work with [Ansible](https://www.ansible.com/) `2.9.23`, it is likely that other versions would work too.

There [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.

A postgres database is required, you can use either a managed database or set up a local one by following these [instructions](./setup-postgres.md).

Modify the [server](./config/server/dev.env.j2) configuration that are required while deploying it.

The 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.


## Playbooks
There are many playbooks involved to let you deploy the necessary pieces only:
- [server.yml](./server.yml) deploys the [server](../server) application to the cloud server.
- [web.yml](./web.yml) deploys the [web](../web) application to the cloud server.
- [admin.yml](./admin.yml) deploys the [admin](../admin) application to the cloud server.
- [nginx.yml](./nginx.yml) installs nginx in the cloud server, which is used to serve the requests from the public internet.
- [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.
- [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.

**NOTE** You will likely run the nginx stuff only once.

After setting up everything:
1. Deploy nginx: `ansible-playbook -i test-hosts.ini nginx.yml`
2. Deploy the apps with: `ansible-playbook -i test-hosts.ini server.yml web.yml admin.yml`
3. Expose the apps to the internet with: `ansible-playbook -i test-hosts.ini nginx_site_admin.yml nginx_site_web.yml`

Once everything is ready, run the first step to deploy the apps again (or use a single playbook to deploy a single app instead).


================================================
FILE: infra/admin.yml
================================================
---
- hosts: frontend
  gather_facts: no
  vars:
    - webapp_source_zip: "apps/admin.zip"
    - webapp_remote_file: "admin.zip"

  tasks:
    - name: Build the application
      shell: ./scripts/build-admin.sh {{ admin_api_url }}
      delegate_to: 127.0.0.1

    - name: Install unzip
      become: yes
      apt:
        name: unzip
        state: latest
        update_cache: yes

    - name: Upload the application
      synchronize:
        src: "{{ webapp_source_zip }}"
        dest: "{{ webapp_remote_file }}"

    - name: Create the admin data directory
      become: yes
      file:
        path: "{{ webapp_admin_assets_directory }}"
        state: directory
        owner: www-data
        group: www-data

    - name: Unpack the application
      become: yes
      unarchive:
        remote_src: yes
        src: admin.zip
        dest: "{{ webapp_admin_assets_directory }}"

    - name: Set the permissions
      become: yes
      file:
        dest: "{{ webapp_admin_assets_directory }}"
        owner: www-data
        group: www-data
        recurse: yes

    - name: Reload nginx config
      become: yes
      service:
        name: nginx
        state: reloaded

================================================
FILE: infra/config/nginx/admin-app-htpasswd
================================================
demo:$apr1$8bJ.PWGf$3IxfPeFYxWRQkCw3yvRfp0

================================================
FILE: infra/config/nginx/admin_app_site.j2
================================================
server {
  listen 80;

  auth_basic "Administrators only";
  auth_basic_user_file {{ nginx_admin_settings_file }};

  server_name {{ admin_app_domain }};
  root {{ webapp_admin_assets_directory }};
  index index.html;

  set_real_ip_from 10.0.0.0/8;
  real_ip_header X-Real-IP;
  real_ip_recursive on;
  client_body_buffer_size 128k;
  proxy_connect_timeout 60;
  proxy_send_timeout 180s;
  proxy_read_timeout 180s;
  proxy_buffer_size 64k;
  proxy_busy_buffers_size 128k;
  proxy_buffers 64 16k;

  location /api {
    proxy_set_header  Host $host;
    proxy_set_header  X-Real-IP $remote_addr;
    proxy_set_header  X-Forwarded-For $remote_addr;
    proxy_set_header  X-Forwarded-Host $remote_addr;
    proxy_set_header  X-Forwarded-User $remote_user;
    proxy_set_header  X-Forwarded-Proto https;
    proxy_http_version 1.1;
    proxy_set_header Connection "";

    rewrite ^/api/(.*) /$1 break;
    proxy_pass http://localhost:{{ server_app_port }};
  }

  # caching static assets
  location ~* \.(jpg|jpeg|png|gif|ico|css|js)$ {
    expires 7d;
  }

  location / {
    try_files $uri $uri/ /index.html;
  }
}


================================================
FILE: infra/config/nginx/mime.types
================================================

types {
    text/html                             html htm shtml;
    text/css                              css;
    text/xml                              xml;
    image/gif                             gif;
    image/jpeg                            jpeg jpg;
    application/javascript                js;
    application/atom+xml                  atom;
    application/rss+xml                   rss;

    text/mathml                           mml;
    text/plain                            txt;
    text/vnd.sun.j2me.app-descriptor      jad;
    text/vnd.wap.wml                      wml;
    text/x-component                      htc;

    image/png                             png;
    image/tiff                            tif tiff;
    image/vnd.wap.wbmp                    wbmp;
    image/x-icon                          ico;
    image/x-jng                           jng;
    image/x-ms-bmp                        bmp;
    image/svg+xml                         svg svgz;
    image/webp                            webp;

    application/font-woff                 woff;
    application/java-archive              jar war ear;
    application/json                      json;
    application/mac-binhex40              hqx;
    application/msword                    doc;
    application/pdf                       pdf;
    application/postscript                ps eps ai;
    application/rtf                       rtf;
    application/vnd.apple.mpegurl         m3u8;
    application/vnd.ms-excel              xls;
    application/vnd.ms-fontobject         eot;
    application/vnd.ms-powerpoint         ppt;
    application/vnd.wap.wmlc              wmlc;
    application/vnd.google-earth.kml+xml  kml;
    application/vnd.google-earth.kmz      kmz;
    application/x-7z-compressed           7z;
    application/x-cocoa                   cco;
    application/x-java-archive-diff       jardiff;
    application/x-java-jnlp-file          jnlp;
    application/x-makeself                run;
    application/x-perl                    pl pm;
    application/x-pilot                   prc pdb;
    application/x-rar-compressed          rar;
    application/x-redhat-package-manager  rpm;
    application/x-sea                     sea;
    application/x-shockwave-flash         swf;
    application/x-stuffit                 sit;
    application/x-tcl                     tcl tk;
    application/x-x509-ca-cert            der pem crt;
    application/x-xpinstall               xpi;
    application/xhtml+xml                 xhtml;
    application/xspf+xml                  xspf;
    application/zip                       zip;

    application/octet-stream              bin exe dll;
    application/octet-stream              deb;
    application/octet-stream              dmg;
    application/octet-stream              iso img;
    application/octet-stream              msi msp msm;

    application/vnd.openxmlformats-officedocument.wordprocessingml.document    docx;
    application/vnd.openxmlformats-officedocument.spreadsheetml.sheet          xlsx;
    application/vnd.openxmlformats-officedocument.presentationml.presentation  pptx;

    audio/midi                            mid midi kar;
    audio/mpeg                            mp3;
    audio/ogg                             ogg;
    audio/x-m4a                           m4a;
    audio/x-realaudio                     ra;

    video/3gpp                            3gpp 3gp;
    video/mp2t                            ts;
    video/mp4                             mp4;
    video/mpeg                            mpeg mpg;
    video/quicktime                       mov;
    video/webm                            webm;
    video/x-flv                           flv;
    video/x-m4v                           m4v;
    video/x-mng                           mng;
    video/x-ms-asf                        asx asf;
    video/x-ms-wmv                        wmv;
    video/x-msvideo                       avi;
}


================================================
FILE: infra/config/nginx/nginx.conf
================================================
user www-data;
worker_processes auto;
pid /run/nginx.pid;

events {
	worker_connections 4096;
	# multi_accept on;
}

http {

	##
	# Basic Settings
	##

	sendfile on;
	tcp_nopush on;
	tcp_nodelay on;
	keepalive_timeout 300s;
	keepalive_requests 2000;

	types_hash_max_size 2048;
	# server_tokens off;

	# server_names_hash_bucket_size 64;
	# server_name_in_redirect off;

	include /etc/nginx/mime.types;
	default_type application/octet-stream;

	##
	# SSL Settings
	##

	ssl_protocols TLSv1 TLSv1.1 TLSv1.2; # Dropping SSLv3, ref: POODLE
	ssl_prefer_server_ciphers on;

	##
	# Logging Settings
	##

	access_log /var/log/nginx/access.log;
	error_log /var/log/nginx/error.log;

	##
	# Gzip Settings
	##

	gzip on;
	gzip_disable "msie6";

	gzip_vary on;
	gzip_proxied any;
	gzip_comp_level 6;
	gzip_buffers 16 8k;
	gzip_http_version 1.1;
	gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;
	gzip_min_length 512;

	##
	# Virtual Host Configs
	##

	include /etc/nginx/conf.d/*.conf;
	include /etc/nginx/sites-enabled/*;
}


================================================
FILE: infra/config/nginx/preview_admin_app_site.j2
================================================
server {

    # listen [::]:443 ssl ipv6only=on; # managed by Certbot
    listen 443 ssl; # managed by Certbot
    ssl_certificate /etc/letsencrypt/live/dev.wiringbits.mx/fullchain.pem; # managed by Certbot
    ssl_certificate_key /etc/letsencrypt/live/dev.wiringbits.mx/privkey.pem; # managed by Certbot
    include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot
    ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot

  auth_basic "Administrators only";
  auth_basic_user_file {{ nginx_admin_settings_file }};

  server_name {{ admin_app_domain }};
  root {{ webapp_admin_assets_directory }};
  index index.html;

  set_real_ip_from 10.0.0.0/8;
  real_ip_header X-Real-IP;
  real_ip_recursive on;
  client_body_buffer_size 128k;
  proxy_connect_timeout 60;
  proxy_send_timeout 180s;
  proxy_read_timeout 180s;
  proxy_buffer_size 64k;
  proxy_busy_buffers_size 128k;
  proxy_buffers 64 16k;

  location /api {
    proxy_set_header  Host $host;
    proxy_set_header  X-Real-IP $remote_addr;
    proxy_set_header  X-Forwarded-For $remote_addr;
    proxy_set_header  X-Forwarded-Host $remote_addr;
    proxy_set_header  X-Forwarded-User $remote_user;
    proxy_set_header  X-Forwarded-Proto https;
    proxy_http_version 1.1;
    proxy_set_header Connection "";

    rewrite ^/api/(.*) /$1 break;
    proxy_pass http://localhost:{{ server_app_port }};
  }

  # caching static assets
  location ~* \.(jpg|jpeg|png|gif|ico|css|js)$ {
    expires 7d;
  }

  location / {
    try_files $uri $uri/ /index.html;
  }
}


server {
    if ($host = {{ admin_app_domain }}) {
        return 301 https://$host$request_uri;
    } # managed by Certbot

  listen 80;

  server_name {{ admin_app_domain }};
    return 404; # managed by Certbot

}


================================================
FILE: infra/config/nginx/preview_web_app_site.j2
================================================
server {

    # listen [::]:443 ssl ipv6only=on; # managed by Certbot
    listen 443 ssl; # managed by Certbot
    ssl_certificate /etc/letsencrypt/live/dev.wiringbits.mx/fullchain.pem; # managed by Certbot
    ssl_certificate_key /etc/letsencrypt/live/dev.wiringbits.mx/privkey.pem; # managed by Certbot
    include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot
    ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot


  server_name {{ web_app_domain }};
  root {{ webapp_assets_directory }};
  index index.html;

  set_real_ip_from 10.0.0.0/8;
  real_ip_header X-Real-IP;
  real_ip_recursive on;
  client_body_buffer_size 128k;
  proxy_connect_timeout 60;
  proxy_send_timeout 180s;
  proxy_read_timeout 180s;
  proxy_buffer_size 64k;
  proxy_busy_buffers_size 128k;
  proxy_buffers 64 16k;

  # swagger docs
  location /swagger.json {
    proxy_pass http://localhost:{{ server_app_port }};
  }

  # the admin api is only reachable when providing the necessary credentials
  location /api/admin {
    auth_basic "Administrators only";
    auth_basic_user_file {{ nginx_admin_settings_file }};

    proxy_set_header  Host $host;
    proxy_set_header  X-Real-IP $remote_addr;
    proxy_set_header  X-Forwarded-For $remote_addr;
    proxy_set_header  X-Forwarded-Host $remote_addr;
    proxy_set_header  X-Forwarded-User $remote_user;
    proxy_set_header  X-Forwarded-Proto https;
    proxy_http_version 1.1;
    proxy_set_header Connection "";

    rewrite ^/api/(.*) /$1 break;
    proxy_pass http://localhost:{{ server_app_port }};
  }

  location /api {
    proxy_set_header  Host $host;
    proxy_set_header  X-Real-IP $remote_addr;
    proxy_set_header  X-Forwarded-For $remote_addr;
    proxy_set_header  X-Forwarded-Host $remote_addr;
    proxy_set_header  X-Forwarded-User $remote_user;
    proxy_set_header  X-Forwarded-Proto https;
    proxy_http_version 1.1;
    proxy_set_header Connection "";

    rewrite ^/api/(.*) /$1 break;
    proxy_pass http://localhost:{{ server_app_port }};
  }

  # FIXME: This prevents returning the static resources from the app, like /api/swagger.json
  # caching static assets
  # location ~* \.(jpg|jpeg|png|gif|ico|css|js)$ {
  #   expires 7d;
  # }

  location / {
    try_files $uri $uri/ /index.html;
  }
}

server {
    if ($host = {{ web_app_domain }}) {
        return 301 https://$host$request_uri;
    } # managed by Certbot

  listen 80;

  server_name {{ web_app_domain }};
    return 404; # managed by Certbot

}


================================================
FILE: infra/config/nginx/web_app_site.j2
================================================
server {
  listen 80;

  server_name {{ web_app_domain }};
  root {{ webapp_assets_directory }};
  index index.html;

  set_real_ip_from 10.0.0.0/8;
  real_ip_header X-Real-IP;
  real_ip_recursive on;
  client_body_buffer_size 128k;
  proxy_connect_timeout 60;
  proxy_send_timeout 180s;
  proxy_read_timeout 180s;
  proxy_buffer_size 64k;
  proxy_busy_buffers_size 128k;
  proxy_buffers 64 16k;

  # swagger docs
  location /swagger.json {
    proxy_pass http://localhost:{{ server_app_port }};
  }

  # the admin api is only reachable when providing the necessary credentials
  location /api/admin {
    auth_basic "Administrators only";
    auth_basic_user_file {{ nginx_admin_settings_file }};

    proxy_set_header  Host $host;
    proxy_set_header  X-Real-IP $remote_addr;
    proxy_set_header  X-Forwarded-For $remote_addr;
    proxy_set_header  X-Forwarded-Host $remote_addr;
    proxy_set_header  X-Forwarded-User $remote_user;
    proxy_set_header  X-Forwarded-Proto https;
    proxy_http_version 1.1;
    proxy_set_header Connection "";

    rewrite ^/api/(.*) /$1 break;
    proxy_pass http://localhost:{{ server_app_port }};
  }

  location /api {
    proxy_set_header  Host $host;
    proxy_set_header  X-Real-IP $remote_addr;
    proxy_set_header  X-Forwarded-For $remote_addr;
    proxy_set_header  X-Forwarded-Host $remote_addr;
    proxy_set_header  X-Forwarded-User $remote_user;
    proxy_set_header  X-Forwarded-Proto https;
    proxy_http_version 1.1;
    proxy_set_header Connection "";

    rewrite ^/api/(.*) /$1 break;
    proxy_pass http://localhost:{{ server_app_port }};
  }

  # FIXME: This prevents returning the static resources from the app, like /api/swagger.json
  # caching static assets
  # location ~* \.(jpg|jpeg|png|gif|ico|css|js)$ {
  #   expires 7d;
  # }

  location / {
    try_files $uri $uri/ /index.html;
  }
}


================================================
FILE: infra/config/server/dev.env.j2
================================================
POSTGRES_DATABASE="server_db"
POSTGRES_USERNAME="db_user"
POSTGRES_PASSWORD="REPLACE_ME"
POSTGRES_HOST="127.0.0.1"

PLAY_APPLICATION_SECRET="REPLACE_ME"
JWT_SECRET="REPLACE_ME"
JWT_ENFORCED=false

PLAY_SESSION_SECURE=true
PLAY_SESSION_DOMAIN="{{ web_app_domain }}"

APP_ALLOWED_HOST_1="{{ web_app_domain }}"
APP_ALLOWED_HOST_2="{{ admin_app_domain }}"

AWS_REGION="us-east-1"
AWS_ACCESS_KEY_ID="REPLACE_ME"
AWS_SECRET_ACCESS_KEY="REPLACE_ME"

EMAIL_SENDER_ADDRESS="template@wiringbits.net"

WEBAPP_HOST="https://template-demo.wiringbits.net"

USER_TOKENS_HMAC_SECRET="REPLACE_ME"


================================================
FILE: infra/config/server/server.service.j2
================================================
[Unit]
Description={{ app_systemd_service_name }}

[Service]
Type=simple
WorkingDirectory={{ app_home }}/{{ app_directory_name }}
StandardOutput=tty
StandardError=tty
EnvironmentFile={{ app_env_config_file }}
LimitNOFILE=65535
User={{ app_user }}
ExecStart={{ app_home }}/{{ app_directory_name }}/bin/{{ app_startup_script }} -Dpidfile.path=/dev/null -Dhttp.port={{ server_app_port }}
Restart=on-failure

[Install]
WantedBy=multi-user.target


================================================
FILE: infra/demo-hosts.ini
================================================
[webapp]
[webapp:vars]
ansible_user=ubuntu
ansible_ssh_extra_args='-o StrictHostKeyChecking=no'

server_app_port=9000

# the domain where the main app is going to run
# you are expected to have already created a DNS "A" record pointing to the server that will host the app
web_app_domain="template-demo.wiringbits.net"

# the domain where the admin app is going to run
# you are expected to have already created a DNS "A" record pointing to the server that will host the app
admin_app_domain="template-demo-admin.wiringbits.net"

[webapp:children]
backend
frontend


[backend]
backend-server ansible_host=64.227.100.33
[backend:vars]
# this is where the environment variables required by the app are defined
# it could be kept encrypted by using ansible-vault
app_env_config_source=config/server/demo.env.j2

# defines the systemd service used to run the app
app_systemd_service_source=config/server/server.service.j2

# the service name used to register the service in systemd, for example,
# restarting the app would be done by invoking: service app-server restart
app_systemd_service_name="wiringbits-server"

# the directory name where the app is stored after building it
# this depends on your app name, get it by running "sbt server/dist" in the app's source
# the last logs will display a line like:
# [info] Your package is ready in .../server/target/universal/wiringbits-server-0.1.0-SNAPSHOT.zip
# the last part is the source name, remove the zip extension and that's the directory name,
# remove the version from the directory name and that's the startup script
app_source_name="wiringbits-server-0.1.0-SNAPSHOT.zip"
app_directory_name="wiringbits-server-0.1.0-SNAPSHOT"
app_startup_script="wiringbits-server"

# user/group/home used to store the app
app_user="play"
app_group="play"
app_home="/home/play/app"
app_env_config_file="/home/play/app/.env"

[frontend]
frontend-server ansible_host=64.227.100.33
[frontend:vars]
# this is necessary to get SSL certificates, it is the email for receiving notifications from letsencrypt
letsencrypt_notifications_email=certbot@wiringbits.net

# the url where the server/backend api is exposed
# this depends on the nginx settings
web_api_url="https://template-demo.wiringbits.net/api"

# the url where the server/backend api is exposed (for the admin website)
# this depends on the nginx settings
admin_api_url="https://template-demo-admin.wiringbits.net/api"

# the settings to enable http basic authorization with nginx while accessing the admin app
# it can be generated by running  `htpasswd`, for defining user called "demo" this could be run:
# - htpasswd -n demo > config/nginx/admin-app-htpasswd
nginx_admin_password_file=config/nginx/admin-app-htpasswd

webapp_assets_directory=/var/www/html
webapp_admin_assets_directory=/var/www/admin


================================================
FILE: infra/nginx.yml
================================================
---
- hosts: frontend
  gather_facts: no
  vars:
    - nginx_config_file: "config/nginx/nginx.conf"
    - nginx_mime_types_file: "config/nginx/mime.types"

  tasks:
    - name: Install nginx
      become: yes
      apt:
        name: nginx
        state: latest
        update_cache: yes

    - name: Disable nginx default site
      become: yes
      file:
        path: /etc/nginx/sites-enabled/default
        state: absent

    - name: Copy the nginx config
      become: yes
      copy:
        src: "{{ nginx_config_file }}"
        dest: /etc/nginx/nginx.conf

    - name: Copy mime.types
      become: yes
      copy:
        src: "{{ nginx_mime_types_file }}"
        dest: /etc/nginx/mime.types

    - name: Restart nginx
      become: yes
      service:
        name: nginx
        state: restarted

    - name: Enable nginx to run on system startup
      become: yes
      systemd:
        name: nginx
        enabled: yes


================================================
FILE: infra/nginx_site_admin.yml
================================================
---
- hosts: frontend
  gather_facts: no
  vars:
    - nginx_site_file: "config/nginx/admin_app_site.j2"
    - nginx_admin_settings_directory: "/etc/nginx/admin-config"
    - nginx_admin_settings_file: "/etc/nginx/admin-config/htpasswd"

  tasks:
    - name: Create the custom config directory
      become: yes
      file:
        path: "{{ nginx_admin_settings_directory }}"
        state: directory

    # file generated with `htpasswd -n demo`, user = demo, pass = wiringbits
    - name: Copy the site password
      become: yes
      copy:
        src: "{{ nginx_admin_password_file }}"
        dest: "{{ nginx_admin_settings_file }}"

    - name: Create the sites-available directory
      become: yes
      file:
        path: /etc/nginx/sites-available
        state: directory

    - name: Copy the site config
      become: yes
      template:
        src: "{{ nginx_site_file }}"
        dest: /etc/nginx/sites-available/{{ admin_app_domain }}

    - name: Create the sites-enabled directory
      become: yes
      file:
        path: /etc/nginx/sites-enabled
        state: directory

    - name: Enable the site
      become: yes
      file:
        src: /etc/nginx/sites-available/{{ admin_app_domain }}
        dest: /etc/nginx/sites-enabled/{{ admin_app_domain }}
        state: link

    - name: Install snapd
      become: yes
      apt:
        name: snapd
        state: latest
        update_cache: yes

    # Get SSL certificate
    # Source: https://certbot.eff.org/lets-encrypt/ubuntufocal-nginx
    - name: Install certbot
      become: yes
      snap:
        name: certbot
        classic: yes

    - name: Get SSL certificate
      become: yes
      shell: certbot -n --agree-tos --nginx -m "{{ letsencrypt_notifications_email }}" --domains "{{ admin_app_domain }}"


================================================
FILE: infra/nginx_site_web.yml
================================================
---
- hosts: frontend
  gather_facts: no
  vars:
    - nginx_site_file: "config/nginx/web_app_site.j2"
    - nginx_admin_settings_file: "/etc/nginx/admin-config/htpasswd"

  tasks:
    - name: Create the sites-available directory
      become: yes
      file:
        path: /etc/nginx/sites-available
        state: directory

    - name: Copy the site config
      become: yes
      template:
        src: "{{ nginx_site_file }}"
        dest: /etc/nginx/sites-available/{{ web_app_domain }}

    - name: Create the sites-enabled directory
      become: yes
      file:
        path: /etc/nginx/sites-enabled
        state: directory

    - name: Enable the site
      become: yes
      file:
        src: /etc/nginx/sites-available/{{ web_app_domain }}
        dest: /etc/nginx/sites-enabled/{{ web_app_domain }}
        state: link

    - name: Install snapd
      become: yes
      apt:
        name: snapd
        state: latest
        update_cache: yes

    # Get SSL certificate
    # Source: https://certbot.eff.org/lets-encrypt/ubuntufocal-nginx
    - name: Install certbot
      become: yes
      snap:
        name: certbot
        classic: yes

    - name: Get SSL certificate
      become: yes
      shell: certbot -n --agree-tos --nginx -m "{{ letsencrypt_notifications_email }}" --domains "{{ web_app_domain }}"


================================================
FILE: infra/preview_nginx_site_admin.yml
================================================
---
- hosts: frontend
  gather_facts: no
  vars:
    - nginx_site_file: "config/nginx/preview_admin_app_site.j2"
    - nginx_admin_settings_directory: "/etc/nginx/admin-config"
    - nginx_admin_settings_file: "/etc/nginx/admin-config/htpasswd"

  tasks:
    - name: Create the custom config directory
      become: yes
      file:
        path: "{{ nginx_admin_settings_directory }}"
        state: directory

    # file generated with `htpasswd -n demo`, user = demo, pass = wiringbits
    - name: Copy the site password
      become: yes
      copy:
        src: "{{ nginx_admin_password_file }}"
        dest: "{{ nginx_admin_settings_file }}"

    - name: Create the sites-available directory
      become: yes
      file:
        path: /etc/nginx/sites-available
        state: directory

    - name: Copy the site config
      become: yes
      template:
        src: "{{ nginx_site_file }}"
        dest: /etc/nginx/sites-available/{{ admin_app_domain }}

    - name: Create the sites-enabled directory
      become: yes
      file:
        path: /etc/nginx/sites-enabled
        state: directory

    - name: Enable the site
      become: yes
      file:
        src: /etc/nginx/sites-available/{{ admin_app_domain }}
        dest: /etc/nginx/sites-enabled/{{ admin_app_domain }}
        state: link

    - name: Reload nginx config
      become: yes
      service:
        name: nginx
        state: reloaded


================================================
FILE: infra/preview_nginx_site_web.yml
================================================
---
- hosts: frontend
  gather_facts: no
  vars:
    - nginx_site_file: "config/nginx/preview_web_app_site.j2"
    - nginx_admin_settings_file: "/etc/nginx/admin-config/htpasswd"

  tasks:
    - name: Create the sites-available directory
      become: yes
      file:
        path: /etc/nginx/sites-available
        state: directory

    - name: Copy the site config
      become: yes
      template:
        src: "{{ nginx_site_file }}"
        dest: /etc/nginx/sites-available/{{ web_app_domain }}

    - name: Create the sites-enabled directory
      become: yes
      file:
        path: /etc/nginx/sites-enabled
        state: directory

    - name: Enable the site
      become: yes
      file:
        src: /etc/nginx/sites-available/{{ web_app_domain }}
        dest: /etc/nginx/sites-enabled/{{ web_app_domain }}
        state: link

    - name: Reload nginx config
      become: yes
      service:
        name: nginx
        state: reloaded


================================================
FILE: infra/scripts/build-admin.sh
================================================
#!/bin/bash
set -e
API_URL=$1
echo "API_URL=$API_URL" \
  && cd ../ \
  && API_URL=$API_URL sbt admin/build \
  && cd -
cd ../admin/build && zip -r admin.zip * && cd -
mkdir -p apps && mv ../admin/build/admin.zip apps/admin.zip


================================================
FILE: infra/scripts/build-server.sh
================================================
#!/bin/bash
set -e
APP_SOURCE_ZIP=$1
echo "APP_SOURCE_ZIP=$APP_SOURCE_ZIP"
cd ../ && SWAGGER_API_BASEPATH="/api" sbt server/dist && cd -
mkdir -p apps && cp ../server/target/universal/$APP_SOURCE_ZIP apps/server.zip


================================================
FILE: infra/scripts/build-web.sh
================================================
#!/bin/bash
set -e
API_URL=$1
echo "API_URL=$API_URL" \
  && cd ../ \
  && API_URL=$API_URL sbt web/build \
  && cd -
cd ../web/build && zip -r web.zip * && cd -
mkdir -p apps && mv ../web/build/web.zip apps/web.zip


================================================
FILE: infra/server.yml
================================================
---
- hosts: backend
  gather_facts: no
  vars:
    - app_source_zip: "apps/server.zip"
    - app_source_zip_remote: "server.zip"

  tasks:
    - name: Install java11
      become: yes
      apt:
        name: default-jre
        state: latest

    - name: Install unzip
      become: yes
      apt:
        name: unzip
        state: latest

    - name: Build the application
      retries: 10
      delay: 5
      shell: "./scripts/build-server.sh {{ app_source_name }}"
      delegate_to: 127.0.0.1

    - name: Upload the application
      synchronize:
        src: "{{ app_source_zip }}"
        dest: "{{ app_source_zip_remote }}"

    # Registering the service before unpacking the app allows to make sure the app is stopped
    # on the first deployment.
    - name: Add the systemd service
      become: yes
      template:
        src: "{{ app_systemd_service_source }}"
        dest: "/etc/systemd/system/{{ app_systemd_service_name }}.service"

    - name: Pick up systemd changes
      become: yes
      systemd:
        daemon_reload: yes

    - name: Make sure the application is stopped
      become: yes
      systemd:
        name: "{{ app_systemd_service_name }}"
        state: stopped

    # This is crucial to avoid polluting the classpath with old jars after upgrading dependencies
    - name: Delete old application (important!)
      become: yes
      file:
        path: "{{ app_home }}/{{ app_directory_name }}"
        state: absent

    - name: Create the app group
      become: yes
      group:
        name: "{{ app_group }}"
        state: present

    - name: Create the app user
      become: yes
      user:
        name: "{{ app_user }}"
        group: "{{ app_group }}"
        state: present
        system: yes

    - name: Create the app directory
      become: yes
      file:
        path: "{{ app_home }}"
        state: directory
        owner: "{{ app_user }}"
        group: "{{ app_group }}"

    - name: Unpack the application
      become: yes
      unarchive:
        remote_src: yes
        src: "{{ app_source_zip_remote }}"
        dest: "{{ app_home }}"
        owner: "{{ app_user }}"
        group: "{{ app_group }}"

    - name: Set the application config
      become: yes
      template:
        src: "{{ app_env_config_source }}"
        dest: "{{ app_env_config_file }}"
        owner: "{{ app_user }}"
        group: "{{ app_group }}"

    - name: Set the application files permissions
      become: yes
      file:
        dest: "{{ app_home }}"
        owner: "{{ app_user }}"
        group: "{{ app_group }}"
        recurse: yes

    - name: Make sure the application is started
      become: yes
      systemd:
        name: "{{ app_systemd_service_name }}"
        state: started

    - name: Enable the application to run on system startup
      become: yes
      systemd:
        name: "{{ app_systemd_service_name }}"
        enabled: yes

# TODO: Check service is healthy by querying localhost:9000/health


================================================
FILE: infra/setup-postgres.md
================================================
# Setup postgres
This is the manual way to set up postgres for a test server, for production it is recommended to use a managed database instead.

Either follow these [instructions](https://postgreshelp.com/postgresql-13-install-in-ubuntu/) or just run these commands:

```bash
# Create the file repository configuration
sudo sh -c 'echo "deb http://apt.postgresql.org/pub/repos/apt $(lsb_release -cs)-pgdg main" > /etc/apt/sources.list.d/pgdg.list'

# Import the repository signing key
wget --quiet -O - https://www.postgresql.org/media/keys/ACCC4CF8.asc | sudo apt-key add -

# Update the package lists
sudo apt-get update

# Install the latest version of PostgreSQL.
# If you want a specific version, use 'postgresql-12' or similar instead of 'postgresql':
sudo apt-get -y install postgresql-15
```

Then, connect to the database (`sudo -u postgres psql`) and create the necessary user/database:

```shell
CREATE DATABASE server_db;
\c server_db;
CREATE EXTENSION IF NOT EXISTS CITEXT;
ALTER DATABASE server_db SET statement_timeout = '60s';
ALTER DATABASE server_db SET idle_in_transaction_session_timeout TO '5min';
CREATE USER db_user WITH SUPERUSER PASSWORD 'useYourOwnPasswordInstead';
GRANT ALL PRIVILEGES ON DATABASE "server_db" to db_user;
```

Test the connection to the new database with the custom user: `psql -h 127.0.0.1 -U db_user server_db`

That's it!


================================================
FILE: infra/test-hosts.ini
================================================
[webapp]
[webapp:vars]
ansible_user=ubuntu
ansible_ssh_extra_args='-o StrictHostKeyChecking=no'

server_app_port=9000

# the domain where the main app is going to run
# you are expected to have already created a DNS "A" record pointing to the server that will host the app
web_app_domain="template-demo.wiringbits.net"

# the domain where the admin app is going to run
# you are expected to have already created a DNS "A" record pointing to the server that will host the app
admin_app_domain="template-demo-admin.wiringbits.net"

[webapp:children]
backend
frontend


[backend]
backend-server ansible_host=64.227.100.33
[backend:vars]
# this is where the environment variables required by the app are defined
# it could be kept encrypted by using ansible-vault
app_env_config_source=config/server/dev.env.j2

# defines the systemd service used to run the app
app_systemd_service_source=config/server/server.service.j2

# the service name used to register the service in systemd, for example,
# restarting the app would be done by invoking: service app-server restart
app_systemd_service_name="wiringbits-server"

# the directory name where the app is stored after building it
# this depends on your app name, get it by running "sbt server/dist" in the app's source
# the last logs will display a line like:
# [info] Your package is ready in .../server/target/universal/wiringbits-server-0.1.0-SNAPSHOT.zip
# the last part is the source name, remove the zip extension and that's the directory name,
# remove the version from the directory name and that's the startup script
app_source_name="wiringbits-server-0.1.0-SNAPSHOT.zip"
app_directory_name="wiringbits-server-0.1.0-SNAPSHOT"
app_startup_script="wiringbits-server"

# user/group/home used to store the app
app_user="play"
app_group="play"
app_home="/home/play/app"
app_env_config_file="/home/play/app/.env"

[frontend]
frontend-server ansible_host=64.227.100.33
[frontend:vars]
# this is necessary to get SSL certificates, it is the email for receiving notifications from letsencrypt
letsencrypt_notifications_email=certbot@wiringbits.net

# the url where the server/backend api is exposed
# this depends on the nginx settings
web_api_url="https://template-demo.wiringbits.net/api"

# the url where the server/backend api is exposed (for the admin website)
# this depends on the nginx settings
admin_api_url="https://template-demo-admin.wiringbits.net/api"

# the settings to enable http basic authorization with nginx while accessing the admin app
# it can be generated by running  `htpasswd`, for defining user called "demo" this could be run:
# - htpasswd -n demo > config/nginx/admin-app-htpasswd
nginx_admin_password_file=config/nginx/admin-app-htpasswd

webapp_assets_directory=/var/www/html
webapp_admin_assets_directory=/var/www/admin


================================================
FILE: infra/web.yml
================================================
---
- hosts: frontend
  gather_facts: no
  vars:
    - webapp_source_zip: "apps/web.zip"
    - webapp_remote_file: "web.zip"

  tasks:
    - name: Build the application
      shell: ./scripts/build-web.sh {{ web_api_url }}
      delegate_to: 127.0.0.1

    - name: Install unzip
      become: yes
      apt:
        name: unzip
        state: latest
        update_cache: yes

    - name: Upload the application
      synchronize:
        src: "{{ webapp_source_zip }}"
        dest: "{{ webapp_remote_file }}"

    - name: Create the web data directory
      become: yes
      file:
        path: "{{ webapp_assets_directory }}"
        state: directory
        owner: www-data
        group: www-data

    - name: Unpack the application
      become: yes
      unarchive:
        remote_src: yes
        src: "{{ webapp_remote_file }}"
        dest: "{{ webapp_assets_directory }}"

    - name: Set the permissions
      become: yes
      file:
        dest: "{{ webapp_assets_directory }}"
        owner: www-data
        group: www-data
        recurse: yes

    - name: Reload nginx config
      become: yes
      service:
        name: nginx
        state: reloaded


================================================
FILE: lib/api/shared/src/main/scala/net/wiringbits/api/ApiClient.scala
================================================
package net.wiringbits.api

import net.wiringbits.api.endpoints.*
import net.wiringbits.api.models.*
import net.wiringbits.api.models.admin.{AdminGetUserLogs, AdminGetUsers}
import net.wiringbits.api.models.auth.{GetCurrentUser, Login, Logout}
import net.wiringbits.api.models.environmentconfig.GetEnvironmentConfig
import net.wiringbits.api.models.users.*
import play.api.libs.json.{Json, Reads}
import sttp.client3.*
import sttp.tapir.PublicEndpoint
import sttp.tapir.client.sttp.SttpClientInterpreter
import sttp.tapir.model.ServerRequest

import java.util.UUID
import scala.concurrent.{ExecutionContext, Future}
import scala.util.{Failure, Success, Try}

object ApiClient {
  case class Config(serverUrl: String)
}

class ApiClient(config: ApiClient.Config)(implicit
    ex: ExecutionContext,
    sttpBackend: SttpBackend[Future, _]
) {
  // While the server requires a userId, it is extracted from the Session cookie, we need a dummy value just to
  // fulfill the method signatures
  private val dummyUserId = Future.successful(UUID.fromString("887a5d77-cb5d-4d9c-b4dc-539c8aae3977"))

  // Similarly to the dummy userId, we need a way to derive the userId from a request, which is used only on the
  // server-side code, this function is helpful to fulfill the method signatures
  private implicit val handleDummyUserId: ServerRequest => Future[UUID] = _ => dummyUserId

  private def asJson[R: Reads](strBody: String) = {
    Try {
      Json.parse(strBody).as[ErrorResponse]
    } match {
      case Success(error) => throw new RuntimeException(error.error)
      case Failure(_) =>
        Try {
          Json.parse(strBody).as[R]
        } match {
          case Success(response) => response
          case Failure(error) => throw new RuntimeException(s"Unexpected response ${error.getMessage}")
        }
    }
  }

  private val ServerAPI = sttp.model.Uri
    .parse(config.serverUrl)
    .getOrElse(throw new RuntimeException("Invalid server url"))

  private val client = SttpClientInterpreter()

  /** This is necessary for non-browser clients, this way, the cookies from the last authentication response are
    * propagated to the next requests
    */
  private var lastAuthResponse = Option.empty[Response[_]]

  private def unsafeSetLoginResponse(response: Response[_]): Unit = synchronized {
    lastAuthResponse = Some(response)
  }

  private def unsafeRemoveLoginResponse(): Unit = synchronized {
    lastAuthResponse = None
  }

  private def handleRequest[I, O](endpoint: PublicEndpoint[I, ErrorResponse, O, Any], request: I): Future[O] = {
    val savedCookies = lastAuthResponse.map(_.unsafeCookies).getOrElse(Seq.empty)

    client
      .toRequestThrowDecodeFailures(endpoint, Some(ServerAPI))
      .apply(request)
      .cookies(savedCookies)
      .send(sttpBackend)
      .map(_.body)
      .map {
        case Left(error) => throw new RuntimeException(error.error)
        case Right(response) => response
      }
  }

  def createUser(request: CreateUser.Request): Future[CreateUser.Response] =
    handleRequest(UsersEndpoints.create, request)

  def verifyEmail(request: VerifyEmail.Request): Future[VerifyEmail.Response] =
    handleRequest(UsersEndpoints.verifyEmail, request)

  def forgotPassword(request: ForgotPassword.Request): Future[ForgotPassword.Response] =
    handleRequest(UsersEndpoints.forgotPassword, request)

  def resetPassword(request: ResetPassword.Request): Future[ResetPassword.Response] =
    handleRequest(UsersEndpoints.resetPassword, request)

  def currentUser: Future[GetCurrentUser.Response] =
    handleRequest(AuthEndpoints.getCurrentUser, dummyUserId)

  def updateUser(request: UpdateUser.Request): Future[UpdateUser.Response] =
    handleRequest(UsersEndpoints.update, (request, dummyUserId))

  def updatePassword(request: UpdatePassword.Request): Future[UpdatePassword.Response] =
    handleRequest(UsersEndpoints.updatePassword, (request, dummyUserId))

  def getUserLogs: Future[GetUserLogs.Response] =
    handleRequest(UsersEndpoints.getLogs, dummyUserId)

  def adminGetUserLogs(userId: UUID): Future[AdminGetUserLogs.Response] =
    handleRequest(AdminEndpoints.getUserLogsEndpoint, ("_", userId, ""))

  def adminGetUsers: Future[AdminGetUsers.Response] =
    handleRequest(AdminEndpoints.getUsersEndpoint, ("_", ""))

  def getEnvironmentConfig: Future[GetEnvironmentConfig.Response] =
    handleRequest(EnvironmentConfigEndpoints.getEnvironmentConfig, ())

  def sendEmailVerificationToken(
      request: SendEmailVerificationToken.Request
  ): Future[SendEmailVerificationToken.Response] =
    handleRequest(UsersEndpoints.sendEmailVerificationToken, request)

  // login and logout are special cases, since they return a cookie, sttp-client can not decode them correctly, so we have
  // to do it manually
  def login(request: Login.Request): Future[Login.Response] =
    client
      .toRequestThrowDecodeFailures(AuthEndpoints.login, Some(ServerAPI))
      .apply(request)
      .response(asStringAlways)
      .send(sttpBackend)
      .map { response =>
        unsafeSetLoginResponse(response)
        response.body
      }
      .map(asJson[Login.Response])

  def logout: Future[Logout.Response] =
    client
      .toRequestThrowDecodeFailures(AuthEndpoints.logout, Some(ServerAPI))
      .apply(dummyUserId)
      .response(asStringAlways)
      .send(sttpBackend)
      .map { response =>
        unsafeRemoveLoginResponse()
        response.body
      }
      .map(asJson[Logout.Response])
}


================================================
FILE: lib/api/shared/src/main/scala/net/wiringbits/api/endpoints/AdminEndpoints.scala
================================================
package net.wiringbits.api.endpoints

import net.wiringbits.api.models
import net.wiringbits.api.models.admin.{AdminGetUserLogs, AdminGetUsers}
import net.wiringbits.api.models.ErrorResponse
import net.wiringbits.common.models.{Email, Name}
import sttp.tapir.*
import sttp.tapir.json.play.*

import java.time.Instant
import java.util.UUID

object AdminEndpoints {
  private val baseEndpoint = endpoint
    .in("admin")
    .tag("Admin")
    .in(adminAuth)
    .errorOut(errorResponseErrorOut)

  val getUserLogsEndpoint: Endpoint[Unit, (String, UUID, String), ErrorResponse, AdminGetUserLogs.Response, Any] =
    baseEndpoint.get
      .in("users" / path[UUID]("userId") / "logs")
      .in(adminHeader)
      .out(
        jsonBody[AdminGetUserLogs.Response].example(
          AdminGetUserLogs.Response(
            List(
              AdminGetUserLogs.Response
                .UserLog(
                  userLogId = UUID.fromString("3fa85f64-5717-4562-b3fc-2c963f66afa6"),
                  message = "Message",
                  createdAt = Instant.parse("2021-01-01T00:00:00Z")
                )
            )
          )
        )
      )
      .errorOut(oneOf(HttpErrors.badRequest, HttpErrors.unauthorized))
      .summary("Get the logs for a specific user")

  val getUsersEndpoint: Endpoint[Unit, (String, String), ErrorResponse, AdminGetUsers.Response, Any] =
    baseEndpoint.get
      .in("users")
      .in(adminHeader)
      .out(
        jsonBody[AdminGetUsers.Response].example(
          AdminGetUsers.Response(
            List(
              AdminGetUsers.Response.User(
                id = UUID.fromString("3fa85f64-5717-4562-b3fc-2c963f66afa6"),
                name = Name.trusted("Alexis"),
                email = Email.trusted("alexis@wiringbits.net"),
                createdAt = Instant.parse("2021-01-01T00:00:00Z")
              )
            )
          )
        )
      )
      .errorOut(oneOf(HttpErrors.badRequest, HttpErrors.unauthorized))
      .summary("Get the registered users")

  val routes: List[AnyEndpoint] = List(
    getUserLogsEndpoint,
    getUsersEndpoint
  )
}


================================================
FILE: lib/api/shared/src/main/scala/net/wiringbits/api/endpoints/AuthEndpoints.scala
================================================
package net.wiringbits.api.endpoints

import net.wiringbits.api.models
import net.wiringbits.api.models.auth.{GetCurrentUser, Login, Logout}
import net.wiringbits.api.models.ErrorResponse
import net.wiringbits.common.models.{Captcha, Email, Name, Password}
import sttp.tapir.*
import sttp.tapir.json.play.*
import sttp.tapir.model.ServerRequest

import java.time.Instant
import java.util.UUID
import scala.concurrent.Future

object AuthEndpoints {
  private val baseEndpoint = endpoint
    .in("auth")
    .tag("Auth")
    .errorOut(errorResponseErrorOut)

  val login: Endpoint[Unit, Login.Request, ErrorResponse, (Login.Response, String), Any] =
    baseEndpoint.post
      .in("login")
      .in(
        jsonBody[Login.Request].example(
          Login.Request(
            email = Email.trusted("alexis@wiringbits.net"),
            password = Password.trusted("notSoWeakPassword"),
            captcha = Captcha.trusted("captcha")
          )
        )
      )
      .out(
        jsonBody[Login.Response]
          .description("Successful login")
          .example(
            Login.Response(
              id = UUID.fromString("3fa85f64-5717-4562-b3fc-2c963f66afa6"),
              name = Name.trusted("Alexis"),
              email = Email.trusted("alexis@wiringbits.net")
            )
          )
      )
      .out(setSessionHeader)
      .errorOut(oneOf(HttpErrors.badRequest))
      .summary("Log into the app")
      .description("Sets a session cookie to authenticate the following requests")

  def logout(implicit
      authHandler: ServerRequest => Future[UUID]
  ): Endpoint[Unit, Future[UUID], ErrorResponse, (Logout.Response, String), Any] =
    baseEndpoint.post
      .in("logout")
      .in(userAuth)
      .out(jsonBody[Logout.Response].description("Successful logout").example(Logout.Response()))
      .out(setSessionHeader)
      .errorOut(oneOf(HttpErrors.badRequest))
      .summary("Logout from the app")
      .description("Clears the session cookie that's stored securely")

  def getCurrentUser(implicit
      authHandler: ServerRequest => Future[UUID]
  ): Endpoint[Unit, Future[UUID], ErrorResponse, GetCurrentUser.Response, Any] =
    baseEndpoint.get
      .in("me")
      .in(userAuth)
      .out(
        jsonBody[GetCurrentUser.Response]
          .description("Got user details")
          .example(
            GetCurrentUser.Response(
              id = UUID.fromString("3fa85f64-5717-4562-b3fc-2c963f66afa6"),
              name = Name.trusted("Alexis"),
              email = Email.trusted("alexis@wiringbits.net"),
              createdAt = Instant.parse("2021-01-01T00:00:00Z")
            )
          )
      )
      .summary("Get the details for the authenticated user")

  def routes(implicit authHandler: ServerRequest => Future[UUID]): List[AnyEndpoint] = List(
    login,
    logout,
    getCurrentUser
  )
}


================================================
FILE: lib/api/shared/src/main/scala/net/wiringbits/api/endpoints/EnvironmentConfigEndpoints.scala
================================================
package net.wiringbits.api.endpoints

import net.wiringbits.api.models
import net.wiringbits.api.models.ErrorResponse
import net.wiringbits.api.models.environmentconfig.GetEnvironmentConfig
import sttp.tapir.*
import sttp.tapir.json.play.*

object EnvironmentConfigEndpoints {
  private val baseEndpoint = endpoint
    .in("environment-config")
    .tag("Misc")
    .errorOut(errorResponseErrorOut)

  val getEnvironmentConfig: Endpoint[Unit, Unit, ErrorResponse, GetEnvironmentConfig.Response, Any] =
    baseEndpoint.get
      .out(
        jsonBody[GetEnvironmentConfig.Response]
          .description("Got the config values")
          .example(GetEnvironmentConfig.Response("siteKey"))
      )
      .summary("Get the config values for the current environment")
      .description("These values are required by the frontend app to interact with the backend")

  val routes: List[AnyEndpoint] = List(
    getEnvironmentConfig
  )
}


================================================
FILE: lib/api/shared/src/main/scala/net/wiringbits/api/endpoints/HealthEndpoints.scala
================================================
package net.wiringbits.api.endpoints

import sttp.tapir.*

object HealthEndpoints {
  private val baseEndpoint = endpoint
    .tag("Misc")
    .in("health")

  val check: Endpoint[Unit, Unit, Unit, Unit, Any] = baseEndpoint.get
    .out(emptyOutput.description("The app is healthy"))
    .summary("Queries the application's health")

  val routes: List[AnyEndpoint] = List(
    check
  )
}


================================================
FILE: lib/api/shared/src/main/scala/net/wiringbits/api/endpoints/UsersEndpoints.scala
================================================
package net.wiringbits.api.endpoints

import net.wiringbits.api.models.*
import net.wiringbits.api.models.users.*
import net.wiringbits.common.models.*
import sttp.tapir.*
import sttp.tapir.json.play.*
import sttp.tapir.model.ServerRequest

import java.time.Instant
import java.util.UUID
import scala.concurrent.Future

object UsersEndpoints {
  private val baseEndpoint = endpoint
    .in("users")
    .tag("Users")
    .errorOut(errorResponseErrorOut)

  val create: Endpoint[Unit, CreateUser.Request, ErrorResponse, CreateUser.Response, Any] = baseEndpoint.post
    .in(
      jsonBody[CreateUser.Request].example(
        CreateUser.Request(
          name = Name.trusted("Alexis"),
          email = Email.trusted("alexis@wiringbits.net"),
          password = Password.trusted("notSoWeakPassword"),
          captcha = Captcha.trusted("captcha")
        )
      )
    )
    .out(
      jsonBody[CreateUser.Response]
        .description("The account was created")
        .example(
          CreateUser.Response(
            id = UUID.fromString("3fa85f64-5717-4562-b3fc-2c963f66afa6"),
            name = Name.trusted("Alexis"),
            email = Email.trusted("alexis@wiringbits.net")
          )
        )
    )
    .errorOut(oneOf(HttpErrors.badRequest))
    .summary("Creates a new account")
    .description("Requires a captcha")

  val verifyEmail: Endpoint[Unit, VerifyEmail.Request, ErrorResponse, VerifyEmail.Response, Any] = baseEndpoint.post
    .in("verify-email")
    .in(
      jsonBody[VerifyEmail.Request].example(
        VerifyEmail.Request(
          UserToken(
            userId = UUID.fromString("3fa85f64-5717-4562-b3fc-2c963f66afa6"),
            token = UUID.fromString("3fa85f64-5717-4562-b3fc-2c963f66afa6")
          )
        )
      )
    )
    .out(jsonBody[VerifyEmail.Response].description("The account's email was verified").example(VerifyEmail.Response()))
    .errorOut(oneOf(HttpErrors.badRequest))
    .summary("Verify the user's email")
    .description(
      "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"
    )

  val forgotPassword: Endpoint[Unit, ForgotPassword.Request, ErrorResponse, ForgotPassword.Response, Any] =
    baseEndpoint.post
      .in("forgot-password")
      .in(
        jsonBody[ForgotPassword.Request].example(
          ForgotPassword.Request(
            email = Email.trusted("alexis@wirngbits.net"),
            captcha = Captcha.trusted("captcha")
          )
        )
      )
      .out(
        jsonBody[ForgotPassword.Response]
          .description("The email to recover the password was sent")
          .example(ForgotPassword.Response())
      )
      .errorOut(oneOf(HttpErrors.badRequest))
      .summary("Requests an email to reset a user password")

  val resetPassword: Endpoint[Unit, ResetPassword.Request, ErrorResponse, ResetPassword.Response, Any] =
    baseEndpoint.post
      .in("reset-password")
      .in(
        jsonBody[ResetPassword.Request]
          .example(
            ResetPassword.Request(
              token = UserToken(
                userId = UUID.fromString("3fa85f64-5717-4562-b3fc-2c963f66afa6"),
                token = UUID.fromString("3fa85f64-5717-4562-b3fc-2c963f66afa6")
              ),
              password = Password.trusted("notSoWeakPassword")
            )
          )
      )
      .out(
        jsonBody[ResetPassword.Response]
          .description("The password was updated")
          .example(
            ResetPassword.Response(
              name = Name.trusted("Alexis"),
              email = Email.trusted("alexis@wiringbits.net")
            )
          )
      )
      .errorOut(oneOf[Unit](HttpErrors.badRequest))
      .summary("Resets a user password")

  val sendEmailVerificationToken
      : Endpoint[Unit, SendEmailVerificationToken.Request, ErrorResponse, SendEmailVerificationToken.Response, Any] =
    baseEndpoint.post
      .in("email-verification-token")
      .in(
        jsonBody[SendEmailVerificationToken.Request].example(
          SendEmailVerificationToken.Request(
            email = Email.trusted("alexis@wiringbits.net"),
            captcha = Captcha.trusted("captcha")
          )
        )
      )
      .out(
        jsonBody[SendEmailVerificationToken.Response]
          .description("The account's email was verified")
          .example(
            SendEmailVerificationToken.Response(
              expiresAt = Instant.parse("2021-01-01T00:00:00Z")
            )
          )
      )
      .errorOut(oneOf(HttpErrors.badRequest))
      .summary("Sends the email verification token")
      .description(
        "The user's email should be unconfirmed, this is intended to re-send a token in case the previous one did not arrive"
      )

  def update(implicit
      authHandler: ServerRequest => Future[UUID]
  ): Endpoint[Unit, (UpdateUser.Request, Future[UUID]), ErrorResponse, UpdateUser.Response, Any] =
    baseEndpoint.put
      .in("me")
      .in(
        jsonBody[UpdateUser.Request].example(
          UpdateUser.Request(
            name = Name.trusted("Alexis")
          )
        )
      )
      .in(userAuth)
      .out(jsonBody[UpdateUser.Response].description("The user details were updated").example(UpdateUser.Response()))
      .errorOut(oneOf(HttpErrors.badRequest))
      .summary("Updates the authenticated user details")

  def updatePassword(implicit
      authHandler: ServerRequest => Future[UUID]
  ): Endpoint[Unit, (UpdatePassword.Request, Future[UUID]), ErrorResponse, UpdatePassword.Response, Any] =
    baseEndpoint.put
      .in("me" / "password")
      .in(
        jsonBody[UpdatePassword.Request]
          .description("The user password was updated")
          .example(
            UpdatePassword.Request(
              oldPassword = Password.trusted("oldWeakPassword"),
              newPassword = Password.trusted("newNotSoWeakPassword")
            )
          )
      )
      .in(userAuth)
      .out(jsonBody[UpdatePassword.Response])
      .errorOut(oneOf(HttpErrors.badRequest))
      .summary("Updates the authenticated user password")

  def getLogs(implicit
      authHandler: ServerRequest => Future[UUID]
  ): Endpoint[Unit, Future[UUID], ErrorResponse, GetUserLogs.Response, Any] = baseEndpoint.get
    .in("me" / "logs")
    .in(userAuth)
    .out(
      jsonBody[GetUserLogs.Response]
        .description("Got user logs")
        .example(
          GetUserLogs.Response(
            List(
              GetUserLogs.Response.UserLog(
                userLogId = UUID.fromString("3fa85f64-5717-4562-b3fc-2c963f66afa6"),
                message = "Message",
                createdAt = Instant.parse("2021-01-01T00:00:00Z")
              )
            )
          )
        )
    )
    .errorOut(oneOf(HttpErrors.badRequest))
    .summary("Get the logs for the authenticated user")

  def routes(implicit authHandler: ServerRequest => Future[UUID]): List[AnyEndpoint] = List(
    create,
    verifyEmail,
    forgotPassword,
    resetPassword,
    sendEmailVerificationToken,
    update,
    updatePassword,
    getLogs
  )
}


================================================
FILE: lib/api/shared/src/main/scala/net/wiringbits/api/endpoints/package.scala
================================================
package net.wiringbits.api

import net.wiringbits.api.models.{ErrorResponse, errorResponseFormat}
import sttp.model.StatusCode
import sttp.tapir.*
import sttp.tapir.EndpointInput.AuthType
import sttp.tapir.generic.auto.*
import sttp.tapir.json.play.*
import sttp.tapir.model.ServerRequest

import java.util.UUID
import scala.concurrent.Future

package object endpoints {
  // TODO: better name?
  object HttpErrors {
    val badRequest: EndpointOutput.OneOfVariant[Unit] = oneOfVariant(
      statusCode(StatusCode.BadRequest).description("Invalid or missing arguments")
    )

    val unauthorized: EndpointOutput.OneOfVariant[Unit] = oneOfVariant(
      statusCode(StatusCode.Unauthorized).description("Invalid or missing authentication")
    )
  }

  val adminHeader: EndpointIO.Header[String] = header[String]("X-Forwarded-User")
    .default("Unknown")
    .schema(_.hidden(true))

  val adminAuth: EndpointInput.Auth[String, AuthType.Http] = auth
    .basic[String]()
    .securitySchemeName("Basic authorization")
    .description("Admin credentials")

  val setSessionHeader: EndpointIO.Header[String] = header[String]("Set-Cookie")
    .description("Set user session")
    .schema(_.hidden(true))

  val errorResponseErrorOut: EndpointIO.Body[String, ErrorResponse] = jsonBody[ErrorResponse]
    .description("Error response")
    .example(ErrorResponse("Unauthorized: Invalid or missing authentication"))
    .schema(_.hidden(true))

  def userAuth(implicit handleAuth: ServerRequest => Future[UUID]): EndpointInput.ExtractFromRequest[Future[UUID]] =
    extractFromRequest(handleAuth)
}


================================================
FILE: lib/api/shared/src/main/scala/net/wiringbits/api/models/PlayErrorResponse.scala
================================================
package net.wiringbits.api.models

import play.api.libs.json.{Format, Json}
import sttp.tapir.Schema

// play json errors are like:
// {"error":{"requestId":2,"message":"Invalid Json: ..."}}

case class PlayErrorResponse(error: PlayErrorResponse.PlayError)

object PlayErrorResponse {
  case class PlayError(message: String)

  implicit val playErrorResponseErrorFormat: Format[PlayError] = Json.format[PlayError]
  implicit val playErrorResponseFormat: Format[PlayErrorResponse] = Json.format[PlayErrorResponse]

  implicit val playErrorResponseErrorSchema: Schema[PlayError] =
    Schema.derived[PlayError].name(Schema.SName("PlayError"))
  implicit val playErrorResponseSchema: Schema[PlayErrorResponse] = Schema
    .derived[PlayErrorResponse]
    .name(Schema.SName("PlayErrorResponse"))
    .description("Response with an application error")
}


================================================
FILE: lib/api/shared/src/main/scala/net/wiringbits/api/models/admin/AdminGetUserLogs.scala
================================================
package net.wiringbits.api.models.admin

import net.wiringbits.api.models.*
import play.api.libs.json.{Format, Json}
import sttp.tapir.Schema
import sttp.tapir.generic.auto.*

import java.time.Instant
import java.util.UUID

object AdminGetUserLogs {
  case class Response(data: List[Response.UserLog])
  implicit val adminGetUserLogsResponseFormat: Format[Response] = Json.format[Response]
  implicit val adminGetUserLogsResponseSchema: Schema[Response] = Schema
    .derived[Response]
    .name(Schema.SName("AdminGetUserLogsResponse"))
    .description("Includes the logs for a single user")

  object Response {
    case class UserLog(userLogId: UUID, message: String, createdAt: Instant)
    implicit val adminGetUserLogsResponseUserLogFormat: Format[UserLog] = Json.format[UserLog]
    implicit val adminGetUserLogsResponseUserLogSchema: Schema[UserLog] = Schema
      .derived[UserLog]
      .name(Schema.SName("AdminGetUserLogsResponseUserLog"))
  }
}


================================================
FILE: lib/api/shared/src/main/scala/net/wiringbits/api/models/admin/AdminGetUsers.scala
================================================
package net.wiringbits.api.models.admin

import net.wiringbits.api.models.*
import net.wiringbits.common.models.{Email, Name}
import play.api.libs.json.{Format, Json}
import sttp.tapir.Schema
import sttp.tapir.generic.auto.*

import java.time.Instant
import java.util.UUID

object AdminGetUsers {

  case class Response(data: List[Response.User])
  implicit val adminGetUsersResponseFormat: Format[Response] = Json.format[Response]
  implicit val adminGetUsersResponseSchema: Schema[Response] = Schema
    .derived[Response]
    .name(Schema.SName("AdminGetUsersResponse"))
    .description("Includes the user list")

  object Response {
    case class User(id: UUID, name: Name, email: Email, createdAt: Instant)
    implicit val adminGetUsersResponseUserFormat: Format[User] = Json.format[User]
    implicit val adminGetUsersResponseUserSchema: Schema[User] = Schema
      .derived[User]
      .name(Schema.SName("AdminGetUsersResponseUser"))
  }
}


================================================
FILE: lib/api/shared/src/main/scala/net/wiringbits/api/models/auth/GetCurrentUser.scala
================================================
package net.wiringbits.api.models.auth

import net.wiringbits.api.models.*
import net.wiringbits.common.models.{Email, Name}
import play.api.libs.json.{Format, Json}
import sttp.tapir.Schema

import java.time.Instant
import java.util.UUID

object GetCurrentUser {
  case class Response(id: UUID, name: Name, email: Email, createdAt: Instant)

  implicit val getUserResponseFormat: Format[Response] = Json.format[Response]

  implicit val getUserResponseSchema: Schema[Response] = Schema
    .derived[Response]
    .name(Schema.SName("GetCurrentUserResponse"))
    .description("Response to find the authenticated user details")
}


================================================
FILE: lib/api/shared/src/main/scala/net/wiringbits/api/models/auth/Login.scala
================================================
package net.wiringbits.api.models.auth

import net.wiringbits.api.models.*
import net.wiringbits.common.models.{Captcha, Email, Name, Password}
import play.api.libs.json.{Format, Json}
import sttp.tapir.Schema
import sttp.tapir.generic.auto.*

import java.util.UUID

object Login {

  case class Request(email: Email, password: Password, captcha: Captcha)

  case class Response(id: UUID, name: Name, email: Email)

  implicit val loginRequestFormat: Format[Request] = Json.format[Request]
  implicit val loginResponseFormat: Format[Response] = Json.format[Response]

  implicit val loginRequestSchema: Schema[Request] = Schema
    .derived[Request]
    .name(Schema.SName("LoginRequest"))
    .description("Request to log into the app")
  implicit val loginResponseSchema: Schema[Response] = Schema
    .derived[Response]
    .name(Schema.SName("LoginResponse"))
    .description("Response after logging into the app")
}


================================================
FILE: lib/api/shared/src/main/scala/net/wiringbits/api/models/auth/Logout.scala
================================================
package net.wiringbits.api.models.auth

import play.api.libs.json.{Format, Json}
import sttp.tapir.Schema

object Logout {

  case class Request(noData: String = "")

  case class Response(noData: String = "")

  implicit val logoutRequestFormat: Format[Request] = Json.format[Request]
  implicit val logoutResponseFormat: Format[Response] = Json.format[Response]

  implicit val logoutRequestSchema: Schema[Request] = Schema
    .derived[Request]
    .name(Schema.SName("LogoutRequest"))
    .description("Request to log out of the app")
  implicit val logoutResponseSchema: Schema[Response] = Schema
    .derived[Response]
    .name(Schema.SName("LogoutResponse"))
    .description("Response after logging out of the app")
}


================================================
FILE: lib/api/shared/src/main/scala/net/wiringbits/api/models/environmentconfig/GetEnvironmentConfig.scala
================================================
package net.wiringbits.api.models.environmentconfig

import play.api.libs.json.{Format, Json}
import sttp.tapir.Schema

object GetEnvironmentConfig {
  case class Response(recaptchaSiteKey: String)

  implicit val configResponseFormat: Format[Response] = Json.format[Response]

  implicit val configResponseSchema: Schema[Response] = Schema
    .derived[Response]
    .name(Schema.SName("GetEnvironmentConfigResponse"))
    .description("Request to fetch the environment config")
}


================================================
FILE: lib/api/shared/src/main/scala/net/wiringbits/api/models/package.scala
================================================
package net.wiringbits.api

import net.wiringbits.webapp.common.models.WrappedString
import play.api.libs.json.*
import sttp.tapir.generic.auto.*
import sttp.tapir.{Schema, SchemaType}

import java.time.Instant

package object models {

  /** For some reason, play-json doesn't provide support for Instant in the scalajs version, grabbing the jvm values
    * seems to work:
    *   - https://github.com/playframework/play-json/blob/master/play-json/jvm/src/main/scala/play/api/libs/json/EnvReads.scala
    *   - https://github.com/playframework/play-json/blob/master/play-json/jvm/src/main/scala/play/api/libs/json/EnvWrites.scala
    */
  implicit val instantFormat: Format[Instant] = Format[Instant](
    fjs = implicitly[Reads[String]].map(string => Instant.parse(string)),
    tjs = Writes[Instant](i => JsString(i.toString))
  )

  case class ErrorResponse(error: String)
  implicit val errorResponseFormat: Format[ErrorResponse] = Json.format[ErrorResponse]
  implicit val errorResponseSchema: Schema[ErrorResponse] = Schema
    .derived[ErrorResponse]
    .name(Schema.SName("ErrorResponse"))

  implicit def wrappedStringSchema[T <: WrappedString]: Schema[T] = Schema(SchemaType.SString())
}


================================================
FILE: lib/api/shared/src/main/scala/net/wiringbits/api/models/users/CreateUser.scala
================================================
package net.wiringbits.api.models.users

import net.wiringbits.api.models.*
import net.wiringbits.common.models.{Captcha, Email, Name, Password}
import play.api.libs.json.{Format, Json}
import sttp.tapir.Schema

import java.util.UUID

object CreateUser {
  case class Request(name: Name, email: Email, password: Password, captcha: Captcha)
  case class Response(id: UUID, name: Name, email: Email)

  implicit val createUserRequestFormat: Format[Request] = Json.format[Request]
  implicit val createUserResponseFormat: Format[Response] = Json.format[Response]

  implicit val createUserRequestSchema: Schema[Request] = Schema
    .derived[Request]
    .name(Schema.SName("CreateUserRequest"))
    .description("Request for the create user API")
  implicit val createUserResponseSchema: Schema[Response] = Schema
    .derived[Response]
    .name(Schema.SName("CreateUserResponse"))
    .description("Response for the create user API")
}


================================================
FILE: lib/api/shared/src/main/scala/net/wiringbits/api/models/users/ForgotPassword.scala
================================================
package net.wiringbits.api.models.users

import net.wiringbits.api.models.*
import net.wiringbits.common.models.{Captcha, Email}
import play.api.libs.json.{Format, Json}
import sttp.tapir.Schema

object ForgotPassword {
  case class Request(email: Email, captcha: Captcha)

  case class Response(noData: String = "")

  implicit val forgotPasswordRequestFormat: Format[Request] = Json.format[Request]
  implicit val forgotPasswordResponseFormat: Format[Response] = Json.format[Response]

  implicit val forgotPasswordRequestSchema: Schema[Request] = Schema
    .derived[Request]
    .name(Schema.SName("ForgotPasswordRequest"))
    .description("Request to reset a forgotten password")
  implicit val forgotPasswordResponseSchema: Schema[Response] = Schema
    .derived[Response]
    .name(Schema.SName("ForgotPasswordResponse"))
    .description("Response to the ForgotPasswordRequest")
}


================================================
FILE: lib/api/shared/src/main/scala/net/wiringbits/api/models/users/GetUserLogs.scala
================================================
package net.wiringbits.api.models.users

import net.wiringbits.api.models.*
import play.api.libs.json.{Format, Json}
import sttp.tapir.Schema
import sttp.tapir.generic.auto.*

import java.time.Instant
import java.util.UUID

object GetUserLogs {
  case class Response(data: List[Response.UserLog])

  object Response {
    case class UserLog(userLogId: UUID, message: String, createdAt: Instant)
    implicit val getUserLogsResponseFormat: Format[UserLog] = Json.format[UserLog]
    implicit val getUserLogsResponseSchema: Schema[UserLog] =
      Schema.derived[UserLog].name(Schema.SName("GetUserLogsResponseUserLog"))
  }

  implicit val getUserLogsResponseFormat: Format[Response] = Json.format[Response]

  implicit val getUserLogsResponseSchema: Schema[Response] = Schema
    .derived[Response]
    .name(Schema.SName("GetUserLogsResponse"))
    .description("Includes the authenticated user logs")
}


================================================
FILE: lib/api/shared/src/main/scala/net/wiringbits/api/models/users/ResetPassword.scala
================================================
package net.wiringbits.api.models.users

import net.wiringbits.api.models.*
import net.wiringbits.common.models.{Email, Name, Password, UserToken}
import play.api.libs.json.{Format, Json}
import sttp.tapir.Schema
import sttp.tapir.generic.auto.*

object ResetPassword {

  case class Request(token: UserToken, password: Password)

  case class Response(name: Name, email: Email)

  implicit val resetPasswordRequestFormat: Format[Request] = Json.format[Request]
  implicit val resetPasswordResponseFormat: Format[Response] = Json.format[Response]

  implicit val resetPasswordRequestSchema: Schema[Request] = Schema
    .derived[Request]
    .name(Schema.SName("ResetPasswordRequest"))
    .description("Request to reset a user password")
  implicit val resetPasswordResponseSchema: Schema[Response] = Schema
    .derived[Response]
    .name(Schema.SName("ResetPasswordResponse"))
    .description("Response after resetting a user password")
}


================================================
FILE: lib/api/shared/src/main/scala/net/wiringbits/api/models/users/SendEmailVerificationToken.scala
================================================
package net.wiringbits.api.models.users

import net.wiringbits.api.models.*
import net.wiringbits.common.models.{Captcha, Email}
import play.api.libs.json.{Format, Json}
import sttp.tapir.Schema

import java.time.Instant

object SendEmailVerificationToken {

  case class Request(email: Email, captcha: Captcha)

  case class Response(expiresAt: Instant)

  implicit val sendEmailVerificationTokenRequestFormat: Format[Request] = Json.format[Request]
  implicit val sendEmailVerificationTokenResponseFormat: Format[Response] = Json.format[Response]

  implicit val sendEmailVerificationTokenRequestSchema: Schema[Request] = Schema
    .derived[Request]
    .name(Schema.SName("SendEmailVerificationTokenRequest"))
    .description("Request to re-send the token to verify an email")
  implicit val sendEmailVerificationTokenResponseSchema: Schema[Response] = Schema
    .derived[Response]
    .name(Schema.SName("SendEmailVerificationTokenResponse"))
    .description("Response after sending the token to verify an email")
}


================================================
FILE: lib/api/shared/src/main/scala/net/wiringbits/api/models/users/UpdatePassword.scala
================================================
package net.wiringbits.api.models.users

import net.wiringbits.api.models.*
import net.wiringbits.common.models.Password
import play.api.libs.json.{Format, Json}
import sttp.tapir.Schema

object UpdatePassword {

  case class Request(oldPassword: Password, newPassword: Password)

  case class Response(noData: String = "")

  implicit val updatePasswordRequestFormat: Format[Request] = Json.format[Request]
  implicit val updatePasswordResponseFormat: Format[Response] = Json.format[Response]

  implicit val updatePasswordRequestSchema: Schema[Request] = Schema
    .derived[Request]
    .name(Schema.SName("UpdatePasswordRequest"))
    .description("Request to change the user's password")
  implicit val updatePasswordResponseSchema: Schema[Response] = Schema
    .derived[Response]
    .name(Schema.SName("UpdatePasswordResponse"))
    .description("Response after updating the user's password")
}


================================================
FILE: lib/api/shared/src/main/scala/net/wiringbits/api/models/users/UpdateUser.scala
================================================
package net.wiringbits.api.models.users

import net.wiringbits.api.models.*
import net.wiringbits.common.models.Name
import play.api.libs.json.{Format, Json}
import sttp.tapir.Schema

object UpdateUser {

  case class Request(name: Name)

  case class Response(noData: String = "")

  implicit val updateUserRequestFormat: Format[Request] = Json.format[Request]
  implicit val updateUserResponseFormat: Format[Response] = Json.format[Response]

  implicit val updateUserRequestSchema: Schema[Request] = Schema
    .derived[Request]
    .name(Schema.SName("UpdateUserRequest"))
    .description("Request to update user details")
  implicit val updateUserResponseSchema: Schema[Response] = Schema
    .derived[Response]
    .name(Schema.SName("UpdateUserResponse"))
    .description("Response after updating the user details")
}


================================================
FILE: lib/api/shared/src/main/scala/net/wiringbits/api/models/users/VerifyEmail.scala
================================================
package net.wiringbits.api.models.users

import net.wiringbits.common.models.UserToken
import play.api.libs.json.{Format, Json}
import sttp.tapir.Schema
import sttp.tapir.generic.auto.*

object VerifyEmail {

  case class Request(token: UserToken)

  case class Response(noData: String = "")

  implicit val verifyEmailRequestFormat: Format[Request] = Json.format[Request]
  implicit val verifyEmailResponseFormat: Format[Response] = Json.format[Response]

  implicit val verifyEmailRequestSchema: Schema[Request] = Schema
    .derived[Request]
    .name(Schema.SName("VerifyEmailRequest"))
    .description("Request to verify an email")
  implicit val verifyEmailResponseSchema: Schema[Response] = Schema
    .derived[Response]
    .name(Schema.SName("VerifyEmailResponse"))
    .description("Response after verifying an email")
}


================================================
FILE: lib/api/shared/src/main/scala/net/wiringbits/api/utils/Formatter.scala
================================================
package net.wiringbits.api.utils

import java.time.Instant

object Formatter {

  def instant(item: Instant): String = {
    try {
      java.time.ZonedDateTime
        .ofInstant(item, java.time.ZoneId.systemDefault())
        .format(java.time.format.DateTimeFormatter.ofPattern("dd/MMM/uuuu hh:mm a"))
    } catch {
      // if for any reason the locale is not available in the sjs libraries, the operation will fail
      // this shouldn't happen in the jvm
      case _: Throwable => item.toString
    }
  }
}


================================================
FILE: lib/common/js/src/test/scala/java/security/SecureRandom.scala
================================================
package java.security

import scala.scalajs.js
import scala.scalajs.js.typedarray.*

// DISCLAIMER: This is almost identical to the official library https://github.com/scala-js/scala-js-fake-insecure-java-securerandom
// There was the need to apply a patch that won't be accepted by the upstream library, given that this is used
// only for tests, it shouldn't be a problem to keep the patch.
//
// The seed in java.util.Random will be unused, so set to 0L instead of having to generate one
class SecureRandom() extends java.util.Random(0L) {
  // Make sure to resolve the appropriate function no later than the first instantiation
  private val getRandomValuesFun = SecureRandom.getRandomValuesFun

  /* setSeed has no effect. For cryptographically secure PRNGs, giving a seed
   * can only ever increase the entropy. It is never allowed to decrease it.
   * Given that we don't have access to an API to strengthen the entropy of the
   * underlying PRNG, it's fine to ignore it instead.
   *
   * Note that the doc of `SecureRandom` says that it will seed itself upon
   * first call to `nextBytes` or `next`, if it has not been seeded yet. This
   * suggests that an *initial* call to `setSeed` would make a `SecureRandom`
   * instance deterministic. Experimentally, this does not seem to be the case,
   * however, so we don't spend extra effort to make that happen.
   */
  override def setSeed(x: Long): Unit = ()

  override def nextBytes(bytes: Array[Byte]): Unit = {
    val len = bytes.length
    val buffer = new Int8Array(len)
    getRandomValuesFun(buffer)
    var i = 0
    while (i != len) {
      bytes(i) = buffer(i)
      i += 1
    }
  }

  override protected final def next(numBits: Int): Int = {
    if (numBits <= 0) {
      0 // special case because the formula on the last line is incorrect for numBits == 0
    } else {
      val buffer = new Int32Array(1)
      getRandomValuesFun(buffer)
      val rand32 = buffer(0)
      rand32 & (-1 >>> (32 - numBits)) // Clear the (32 - numBits) higher order bits
    }
  }
}

object SecureRandom {
  private lazy val getRandomValuesFun: js.Function1[ArrayBufferView, Unit] = {
    if (
      js.typeOf(js.Dynamic.global.crypto) != "undefined" &&
      js.typeOf(js.Dynamic.global.crypto.getRandomValues) == "function"
    ) {
      { (buffer: ArrayBufferView) =>
        js.Dynamic.global.crypto.getRandomValues(buffer)
        ()
      }
    } else if (js.typeOf(js.Dynamic.global.require) == "function") {
      try {
        val crypto = js.Dynamic.global.require("crypto")
        if (js.typeOf(crypto.randomFillSync) == "function") {
          { (buffer: ArrayBufferView) =>
            /** This part differs from the official implementation because it catches runtime exceptions
              *
              * This was necessary because webpack seems to be polluting our runtime libraries with one that breaks in
              * the tests.
              */
            try {
              crypto.randomFillSync(buffer)
            } catch {
              case _: Throwable => insecureDefault(buffer)
            }
            ()
          }
        } else {
          insecureDefault
        }
      } catch {
        case _: Throwable =>
          insecureDefault
      }
    } else {
      insecureDefault
    }
  }

  private def insecureDefault: js.Function1[ArrayBufferView, Unit] = {
    val insecureRandom = new java.util.Random()

    { (buffer: ArrayBufferView) =>
      val asInt8Array = new Int8Array(buffer.buffer, buffer.byteOffset, buffer.byteLength)
      val len = asInt8Array.length
      val arrayBuffer = new Array[Byte](len)
      insecureRandom.nextBytes(arrayBuffer)
      var i = 0
      while (i != len) {
        asInt8Array(i) = arrayBuffer(i)
        i += 1
      }
    }
  }
}


================================================
FILE: lib/common/shared/src/main/scala/net/wiringbits/common/ErrorMessages.scala
================================================
package net.wiringbits.common

object ErrorMessages {
  val emailNotVerified = "The email is not verified, check your spam folder if you don't see the email."
}


================================================
FILE: lib/common/shared/src/main/scala/net/wiringbits/common/models/Captcha.scala
================================================
package net.wiringbits.common.models

import net.wiringbits.webapp.common.models.WrappedString
import net.wiringbits.webapp.common.validators.ValidationResult

class Captcha private (val string: String) extends WrappedString

object Captcha extends WrappedString.Companion[Captcha] {

  override def validate(string: String): ValidationResult[Captcha] = {

    Option(string.trim)
      .filter(_.nonEmpty)
      .map(ValidationResult.Valid(_, new Captcha(string)))
      .getOrElse {
        ValidationResult.Invalid(string, "Invalid recaptcha")
      }
  }

  override def trusted(string: String): Captcha = new Captcha(string)
}


================================================
FILE: lib/common/shared/src/main/scala/net/wiringbits/common/models/Email.scala
================================================
package net.wiringbits.common.models

import net.wiringbits.webapp.common.models.WrappedString
import net.wiringbits.webapp.common.validators.ValidationResult

class Email private (val string: String) extends WrappedString

object Email extends WrappedString.Companion[Email] {

  private val emailRegex =
    """^[\w.!#$%&'*+/=?^_`{|}~-]+@([\w-]+\.)+[\w-]{2,7}$""".r

  override def validate(string: String): ValidationResult[Email] = {
    val valid = emailRegex.findAllMatchIn(string).length == 1
    Option
      .when(valid)(ValidationResult.Valid(string, new Email(string)))
      .getOrElse {
        ValidationResult.Invalid(string, "Invalid email")
      }
  }

  override def trusted(string: String): Email = new Email(string)
}


================================================
FILE: lib/common/shared/src/main/scala/net/wiringbits/common/models/Name.scala
================================================
package net.wiringbits.common.models

import net.wiringbits.webapp.common.models.WrappedString
import net.wiringbits.webapp.common.validators.ValidationResult

class Name private (val string: String) extends WrappedString

object Name extends WrappedString.Companion[Name] {

  private val minNameLength: Int = 2 // we do have people named like `Jo`

  override def validate(string: String): ValidationResult[Name] = {
    val isValid = string.length >= minNameLength

    Option
      .when(isValid)(ValidationResult.Valid(string, new Name(string)))
      .getOrElse {
        ValidationResult.Invalid(string, "Invalid name")
      }
  }

  override def trusted(string: String): Name = new Name(string)
}


================================================
FILE: lib/common/shared/src/main/scala/net/wiringbits/common/models/Password.scala
================================================
package net.wiringbits.common.models

import net.wiringbits.webapp.common.models.WrappedString
import net.wiringbits.webapp.common.validators.ValidationResult

class Password private (val string: String) extends WrappedString

object Password extends WrappedString.Companion[Password] {
  private val minPasswordLength: Int = 8

  override def validate(string: String): ValidationResult[Password] = {
    val isValid = string.length >= minPasswordLength

    Option
      .when(isValid)(ValidationResult.Valid(string, new Password(string)))
      .getOrElse {
        ValidationResult.Invalid(string, "Invalid password")
      }
  }

  override def trusted(string: String): Password = new Password(string)
}


================================================
FILE: lib/common/shared/src/main/scala/net/wiringbits/common/models/UserToken.scala
================================================
package net.wiringbits.common.models

import play.api.libs.json.{Format, Json}

import java.util.UUID
import scala.util.Try

case class UserToken(userId: UUID, token: UUID)

object UserToken {
  def validate(tokenStr: String): Option[UserToken] = {
    val splittedToken = tokenStr.split("_")
    val isValid = splittedToken.length == 2

    // TODO: Improve this impl
    Try(
      Option.when(isValid)(UserToken(UUID.fromString(splittedToken(0)), UUID.fromString(splittedToken(1))))
    ).toOption.flatten
  }

  implicit val userTokenFormat: Format[UserToken] = Json.format[UserToken]
}


================================================
FILE: lib/common/shared/src/test/scala/net/wiringbits/common/models/EmailSpec.scala
================================================
package net.wiringbits.common.models

import org.scalatest.matchers.must.Matchers.*
import org.scalatest.wordspec.AnyWordSpec

class EmailSpec extends AnyWordSpec {

  val valid = List(
    "alexis@wiringbits.net",
    "a@xe.com",
    "ejemplo@goo.gl",
    "ejemplo+aqui@e.io",
    "one_mail@test.com",
    "valid.mail@test.xs",
    "valid_@gf.com",
    "test@gmail.co.au",
    "test@gmail.space"
  )

  val invalid = List(
    "alexis@wiringbits.net.",
    "alexis@wiringbits.net a@xe.net",
    "esto,noes@unemail",
    "esto tampoco@es",
    "@xe.com",
    "hello@",
    "ejemplo@goo",
    ".",
    ""
  )

  "validate" should {
    valid.foreach { input =>
      s"accept valid values: $input" in {
        Email.validate(input).isValid must be(true)
      }
    }

    invalid.foreach { input =>
      s"reject invalid values: $input" in {
        Email.validate(input).isValid must be(false)
      }
    }
  }
}


================================================
FILE: lib/common/shared/src/test/scala/net/wiringbits/common/models/NameSpec.scala
================================================
package net.wiringbits.common.models

import org.scalatest.matchers.must.Matchers.*
import org.scalatest.wordspec.AnyWordSpec

class NameSpec extends AnyWordSpec {

  val valid = List(
    "ale",
    "jo",
    "jorge julian"
  )

  val invalid = List(
    ".",
    "",
    "a"
  )

  "validate" should {
    valid.foreach { input =>
      s"accept valid values: $input" in {
        Name.validate(input).isValid must be(true)
      }
    }

    invalid.foreach { input =>
      s"reject invalid values: $input" in {
        Name.validate(input).isValid must be(false)
      }
    }
  }
}


================================================
FILE: lib/common/shared/src/test/scala/net/wiringbits/common/models/PasswordSpec.scala
================================================
package net.wiringbits.common.models

import org.scalatest.matchers.must.Matchers.*
import org.scalatest.wordspec.AnyWordSpec

class PasswordSpec extends AnyWordSpec {

  val valid = List(
    "12345678",
    "aaabbbcc",
    "..121l2.1.2o9z9n23 voi109"
  )

  val invalid = List(
    "...11..",
    "",
    "1j190u"
  )

  "validate" should {
    valid.foreach { input =>
      s"accept valid values: $input" in {
        Password.validate(input).isValid must be(true)
      }
    }

    invalid.foreach { input =>
      s"reject invalid values: $input" in {
        Password.validate(input).isValid must be(false)
      }
    }
  }
}


================================================
FILE: lib/common/shared/src/test/scala/net/wiringbits/common/models/UserTokenSpec.scala
================================================
package net.wiringbits.common.models

import org.scalatest.matchers.must.Matchers.{be, must}
import org.scalatest.wordspec.AnyWordSpec

import java.util.UUID

class UserTokenSpec extends AnyWordSpec {

  "validate" should {
    "succeed when there's two valid UUIDs and one underscore" in {
      val valid = s"${UUID.randomUUID()}_${UUID.randomUUID()}"
      UserToken.validate(valid).isDefined must be(true)
    }

    s"fail when the string is not a valid UUID" in {
      val invalid = "wiringbits"
      UserToken.validate(invalid).isDefined must be(false)
    }

    s"fail when the string is not a valid UUID and there's an underscore" in {
      val invalid = "wiringbits_wiringbits"
      UserToken.validate(invalid).isDefined must be(false)
    }

    s"fail when there's zero underscores in the string" in {
      val invalid = UUID.randomUUID.toString
      UserToken.validate(invalid).isDefined must be(false)
    }

    s"fail when there's more than two underscores in the string" in {
      val invalid = s"${UUID.randomUUID()}_${UUID.randomUUID()}_${UUID.randomUUID()}"
      UserToken.validate(invalid).isDefined must be(false)
    }

    s"fail when there's an underscore after the UUID" in {
      val invalid = s"${UUID.randomUUID()}_"
      UserToken.validate(invalid).isDefined must be(false)
    }
  }
}


================================================
FILE: lib/ui/src/main/scala/net/wiringbits/ui/components/core/widgets/ValidatedTextInput.scala
================================================
package net.wiringbits.ui.components.core.widgets

import com.olvind.mui.muiMaterial.components as mui

import net.wiringbits.webapp.common.validators.{TextValidator, ValidationResult}
import net.wiringbits.webapp.utils.slinkyUtils.forms.FormField
import org.scalajs.dom
import slinky.core.FunctionalComponent

abstract class ValidatedTextInput[T: TextValidator] {
  private val validator = implicitly[TextValidator[T]]

  case class Props(
      field: FormField[T],
      disabled: Boolean = false,
      onChange: ValidationResult[T] => Unit,
      margin: "dense" = "dense"
  )

  val component: FunctionalComponent[Props] = FunctionalComponent[Props] { props =>
    def onChange(text: String): Unit = {
      val validation = validator(text)
      props.onChange(validation)
    }

    val helperText = props.field.value.flatMap(_.errorMessage).getOrElse("")
    val value = props.field.value.map(_.input).getOrElse("")
    val hasError = props.field.value.exists(_.hasError)
    mui.TextField
      .outlined()
      .id(s"ExperimentalTextInput-${props.field.name}")
      .name(s"ExperimentalTextInput-${props.field.name}")
      .label(props.field.label)
      .`type`(props.field.`type`)
      .required(props.field.required)
      .fullWidth(true)
      .disabled(props.disabled)
      .margin(props.margin)
      .error(hasError)
      .helperText(helperText)
      .value(value)
      .onChange(e => onChange(e.target.asInstanceOf[dom.HTMLInputElement].value))
  }
}


================================================
FILE: lib/ui/src/main/scala/net/wiringbits/ui/components/inputs/inputs.scala
================================================
package net.wiringbits.ui.components

import net.wiringbits.common.models.{Email, Name, Password}
import net.wiringbits.ui.components.core.widgets.ValidatedTextInput

package object inputs {
  object NameInput extends ValidatedTextInput[Name]
  object EmailInput extends ValidatedTextInput[Email]
  object PasswordInput extends ValidatedTextInput[Password]
}


================================================
FILE: project/build.properties
================================================
sbt.version = 1.7.3


================================================
FILE: project/plugins.sbt
================================================
// while there are some eviction errors, plugins seem to be compatible so far
evictionErrorLevel := sbt.util.Level.Warn

addSbtPlugin("org.portable-scala" % "sbt-scalajs-crossproject" % "1.3.1")

addSbtPlugin("org.playframework" % "sbt-plugin" % "3.0.0")

addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.13.1")

addSbtPlugin("ch.epfl.scala" % "sbt-scalajs-bundler" % "0.20.0")

addSbtPlugin("org.scalablytyped.converter" % "sbt-converter" % "1.0.0-beta39")

addSbtPlugin("com.eed3si9n" % "sbt-buildinfo" % "0.11.0")

addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.5.0")

addSbtPlugin("org.wartremover" % "sbt-wartremover" % "3.1.3")


================================================
FILE: server/src/main/resources/application.conf
================================================
# https://www.playframework.com/documentation/latest/Configuration

# Swagger - be aware these are used at compile time
swagger {
  api {
    basePath = ""
    basePath = ${?SWAGGER_API_BASEPATH}

    info = {
      version = "beta"
      contact = "template@wiringbits.net"
      title = "Scala webapp template's API"
      description = "The API for the Scala webapp template app"
    }
  }
}

play.i18n.langs = ["en"]

play.filters.hosts {
  allowed = [host.docker.internal, "localhost", "localhost:9000", "127.0.0.1:9000"]
  allowed += ${?APP_ALLOWED_HOST_1}
  allowed += ${?APP_ALLOWED_HOST_2}
  allowed += ${?APP_ALLOWED_HOST_3}

}

play.http {
  # Important for production, it is used to sign sessions
  secret.key = "changeme"
  secret.key = ${?PLAY_APPLICATION_SECRET}

  errorHandler = "play.api.http.JsonHttpErrorHandler"

  session {
    cookieName = "__APP_SESSION__"

    # false by default because we use http locally, must be true in prod
    secure = false
    secure = ${?PLAY_SESSION_SECURE}

    # to secure the cookie, this value should be set in prod
    domain = null
    domain = ${?PLAY_SESSION_DOMAIN}

    # The session path
    # Must start with /.
    path = ${play.http.context}
    path = ${?PLAY_SESSION_DOMAIN_PATH}
  }
}

play.filters.disabled += "play.filters.csrf.CSRFFilter"
play.filters.enabled += "play.filters.cors.CORSFilter"


db.default {
  driver = "org.postgresql.Driver"
  host = "localhost:5432"
  database = "wiringbits_db"
  username = "postgres"
  password = "postgres"

  host = ${?POSTGRES_HOST}
  database = ${?POSTGRES_DATABASE}
  username = ${?POSTGRES_USERNAME}
  password = ${?POSTGRES_PASSWORD}

  url = "jdbc:postgresql://"${db.default.host}"/"${db.default.database}
}

play.evolutions {
  autoApply = true

  db.default {
    enabled = true
    # Important because when this is false, failed migrations won't get to the play_evolutions table
    # preventing us to fix them manually
    autocommit = true
  }
}

# Number of database connections
# See https://github.com/brettwooldridge/HikariCP/wiki/About-Pool-Sizing
fixedConnectionPool = 9

play.db {
  prototype {
    hikaricp.minimumIdle = ${fixedConnectionPool}
    hikaricp.maximumPoolSize = ${fixedConnectionPool}
  }
}

# Job queue sized to HikariCP connection pool
database.dispatcher {
  executor = "thread-pool-executor"
  throughput = 1
  thread-pool-executor {
    fixed-pool-size = ${fixedConnectionPool}
  }
}

blocking.dispatcher {
  executor = "thread-pool-executor"
  throughput = 1
  thread-pool-executor {
    // very high bound to process lots of blocking operations concurrently
    fixed-pool-size = 5000
  }
}

play.modules.enabled += "net.wiringbits.modules.ApisModule"
play.modules.enabled += "net.wiringbits.modules.ConfigModule"
play.modules.enabled += "net.wiringbits.modules.ExecutorsModule"
play.modules.enabled += "net.wiringbits.modules.ClockModule"
play.modules.enabled += "net.wiringbits.modules.TasksModule"

email {
  senderAddress = "replace@replace.net"
  senderAddress = ${?EMAIL_SENDER_ADDRESS}

  # defines the provider used to send emails, valid values being "aws" or "none"
  provider = "none"
  provider = ${?EMAIL_PROVIDER}
}

userTokens {
  hmacSecret = "REPLACE ME"
  hmacSecret = ${?USER_TOKENS_HMAC_SECRET}

  emailVerification {
    # expiration time for email verification
    expirationTime = "24 hours"
    expirationTime = ${?USER_TOKENS_EMAIL_VERIFICATION_EXPIRATION_TIME}
  }

  resetPassword {
    # expiration time for email reset password
    expirationTime = "24 hours"
    expirationTime = ${?USER_TOKENS_RESET_PASSWORD_EXPIRATION_TIME}
  }
}

aws {
  region = "us-west-2"
  region = ${?AWS_REGION}

  accessKeyId = REPLACE_ME
  accessKeyId = ${?AWS_ACCESS_KEY_ID}

  secretAccessKey = REPLACE_ME
  secretAccessKey = ${?AWS_SECRET_ACCESS_KEY}
}

webapp {
  host = "http://localhost:8080"
  host = ${?WEBAPP_HOST}
}

recaptcha {
  # secret key only used for test purposes
  secretKey = "6LeIxAcTAAAAAGG-vFI1TnRWxMZNFuojJ4WifJWe"
  secretKey = ${?RECAPTCHA_SECRET_KEY}
  siteKey = "6LeIxAcTAAAAAJcZVRqyHh71UMIEGNQ_MXjiZKhI"
  siteKey = ${?RECAPTCHA_SITE_KEY}
}

backgroundJobsExecutorTask {
  # the task will run every time the period is fullfilled
  interval = 1 minutes
  interval = ${?NOTIFICATIONS_TASK_INTERVAL}
}


================================================
FILE: server/src/main/resources/evolutions/default/1.sql
================================================

-- !Ups


-- The users table has the minimum necessary data
CREATE TABLE users(
  user_id UUID NOT NULL,
  name TEXT NOT NULL,
  last_name TEXT NULL,
  email CITEXT NOT NULL,
  password TEXT NOT NULL,
  created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
  verified_on TIMESTAMPTZ NULL,
  CONSTRAINT users_user_id_pk PRIMARY KEY (user_id),
  CONSTRAINT users_email_unique UNIQUE (email)
);

CREATE INDEX users_email_index ON users USING BTREE (email);

-- create the table to store the user logs
CREATE TABLE user_logs (
    user_log_id UUID NOT NULL,
    user_id UUID NOT NULL,
    message TEXT NOT NULL,
    created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    CONSTRAINT user_logs_pk PRIMARY KEY (user_log_id),
    CONSTRAINT user_logs_users_fk FOREIGN KEY (user_id) REFERENCES users(user_id)
);

CREATE INDEX user_logs_user_id_index ON user_logs USING BTREE (user_id);

CREATE TABLE user_tokens (
    user_token_id UUID NOT NULL,
    token TEXT NOT NULL,
    token_type TEXT NOT NULL,
    created_at TIMESTAMPTZ NOT NULL,
    expires_at TIMESTAMPTZ NOT NULL,
    user_id UUID NOT NULL,
    CONSTRAINT user_tokens_id_pk PRIMARY KEY (user_token_id),
    CONSTRAINT user_tokens_user_id_fk FOREIGN KEY (user_id) REFERENCES users (user_id)
);

CREATE INDEX user_tokens_user_id_index ON user_tokens USING BTREE (user_id);

-- Stores the notifications we are sending to the user from a background job
CREATE TABLE user_notifications (
    user_notification_id UUID NOT NULL,
    user_id UUID NOT NULL,
    notification_type TEXT NOT NULL,
    subject TEXT NOT NULL,
    message TEXT NOT NULL,
    status TEXT NOT NULL, -- pending/success/failed,
    status_details TEXT NULL, -- if failed, what was the reason
    error_count INT DEFAULT 0,
    execute_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    CONSTRAINT user_notifications_user_notification_id_pk PRIMARY KEY (user_notification_id),
    CONSTRAINT user_notifications_user_id_fk FOREIGN KEY (user_id) REFERENCES users(user_id)
);

CREATE INDEX user_notifications_user_id_index ON user_notifications USING BTREE (user_id);
CREATE INDEX user_notifications_execute_at_index ON user_notifications USING BTREE (execute_at);

================================================
FILE: server/src/main/resources/evolutions/default/2.sql
================================================

-- !Ups

-- Stores the background jobs from the app
CREATE TABLE background_jobs (
    background_job_id UUID NOT NULL,
    type TEXT NOT NULL,
    payload JSONB NOT NULL,
    status TEXT NOT NULL, -- pending/success/failed,
    status_details TEXT NULL, -- if failed, what was the reason
    error_count INT DEFAULT 0,
    execute_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    CONSTRAINT background_jobs_id_pk PRIMARY KEY (background_job_id)
);

CREATE INDEX background_jobs_execute_at_index ON background_jobs USING BTREE (execute_at);

-- these are now handled by background_jobs
DROP TABLE user_notifications;


================================================
FILE: server/src/main/resources/logback.xml
================================================
<!-- https://www.playframework.com/documentation/latest/SettingsLogger -->
<configuration>

    <conversionRule conversionWord="coloredLevel" converterClass="play.api.libs.logback.ColoredLevel" />

    <appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <!-- the user.home is used to persist the logs because the application directory is re-created on every deployment -->
        <!-- TODO: Find a way to override this so that the this only affects deployments -->
        <file>${user.home:-.}/logs/application.log</file>

        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <!-- daily rollover -->
            <fileNamePattern>${user.home:-.}/logs/application.%d{yyyy-MM-dd}.log</fileNamePattern>

            <!-- keep 30 days' worth of history capped at 3GB total size -->
            <maxHistory>30</maxHistory>
            <totalSizeCap>3GB</totalSizeCap>
        </rollingPolicy>

        <encoder>
            <pattern>%date [%level] from %logger in %thread - %message%n%rEx%xException </pattern>
        </encoder>
    </appender>

    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <pattern>%coloredLevel %logger{15} - %message%n%rEx%xException{10} </pattern>
        </encoder>
    </appender>

    <appender name="ASYNCFILE" class="ch.qos.logback.classic.AsyncAppender">
        <appender-ref ref="FILE" />
    </appender>

    <appender name="ASYNCSTDOUT" class="ch.qos.logback.classic.AsyncAppender">
        <appender-ref ref="STDOUT" />
    </appender>

    <logger name="play" level="INFO" />
    <logger name="net.wiringbits" level="DEBUG" />
    <logger name="controllers" level="DEBUG" />

    <!-- produces invalid CORS warnings, client is not authorized to call this from its own domain -->
    <logger name="play.filters.cors.CORSFilter" level="ERROR" />
    <!-- produces host not allowed warnings, client is not allowed to call the host directly -->
    <logger name="play.filters.hosts.AllowedHostsFilter" level="ERROR" />
    <!-- produces CSRF warnings when clients send wrong requests -->
    <logger name="play.filters.csrf.CSRFAction" level="ERROR" />

    <!-- Off these ones as they are annoying, and anyway we manage configuration ourselves -->
    <logger name="com.avaje.ebean.config.PropertyMapLoader" level="OFF" />
    <logger name="com.avaje.ebeaninternal.server.core.XmlConfigLoader" level="OFF" />
    <logger name="com.avaje.ebeaninternal.server.lib.BackgroundThread" level="OFF" />
    <logger name="com.gargoylesoftware.htmlunit.javascript" level="OFF" />

    <root level="ERROR">
        <appender-ref ref="ASYNCFILE" />
        <appender-ref ref="ASYNCSTDOUT" />
    </root>

</configuration>


================================================
FILE: server/src/main/resources/messages
================================================
# https://www.playframework.com/documentation/latest/ScalaI18N


================================================
FILE: server/src/main/resources/routes
================================================
# Routes
# This file defines all application routes (Higher priority routes first)
# https://www.playframework.com/documentation/latest/ScalaRouting
# ~~~~

-> / controllers.ApiRouter

# routes for admin tables (GET, POST, PUT and DELETE)
#-> / net.wiringbits.webapp.utils.admin.AppRouter


================================================
FILE: server/src/main/scala/PekkoStream.scala
================================================
/*
 * Copyright (C) from 2022 The Play Framework Contributors <https://github.com/playframework>, 2011-2021 Lightbend Inc. <https://www.lightbend.com>
 */

package anorm

import java.sql.Connection
import scala.util.control.NonFatal
import scala.concurrent.{Future, Promise}
import org.apache.pekko.stream.scaladsl.Source

import scala.annotation.nowarn

/** Anorm companion for the Pekko Streams.
  *
  * @define materialization
  *   It materializes a [[scala.concurrent.Future]] of [[scala.Int]] containing the number of rows read from the source
  *   upon completion, and a possible exception if row parsing failed.
  * @define sqlParam
  *   the SQL query
  * @define connectionParam
  *   the JDBC connection, which must not be closed until the source is materialized.
  * @define columnAliaserParam
  *   the column aliaser
  */
// From https://github.com/playframework/anorm/blob/main/pekko/src/main/scala/anorm/PekkoStream.scala
// We are copying this because the anorm.pekko isn't published yet
// TODO: remove after anorm.pekko is published
object PekkoStream {

  /** Returns the rows parsed from the `sql` query as a reactive source.
    *
    * $materialization
    *
    * @tparam T
    *   the type of the result elements
    * @param sql
    *   $sqlParam
    * @param parser
    *   the result (row) parser
    * @param as
    *   $columnAliaserParam
    * @param connection
    *   $connectionParam
    *
    * {{{
    * import java.sql.Connection
    *
    * import scala.concurrent.Future
    *
    * import org.apache.pekko.stream.scaladsl.Source
    *
    * import anorm._
    *
    * def resultSource(implicit con: Connection): Source[String, Future[Int]] = PekkoStream.source(SQL"SELECT * FROM Test", SqlParser.scalar[String], ColumnAliaser.empty)
    * }}}
    */
  @SuppressWarnings(Array("UnusedMethodParameter"))
  def source[T](sql: => Sql, parser: RowParser[T], as: ColumnAliaser)(implicit
      con: Connection
  ): Source[T, Future[Int]] = Source.fromGraph(new ResultSource[T](con, sql, as, parser))

  /** Returns the rows parsed from the `sql` query as a reactive source.
    *
    * $materialization
    *
    * @tparam T
    *   the type of the result elements
    * @param sql
    *   $sqlParam
    * @param parser
    *   the result (row) parser
    * @param connection
    *   $connectionParam
    */
  @SuppressWarnings(Array("UnusedMethodParameter"))
  def source[T](sql: => Sql, parser: RowParser[T])(implicit con: Connection): Source[T, Future[Int]] =
    source[T](sql, parser, ColumnAliaser.empty)

  /** Returns the result rows from the `sql` query as an enumerator. This is equivalent to `source[Row](sql,
    * RowParser.successful, as)`.
    *
    * $materialization
    *
    * @param sql
    *   $sqlParam
    * @param as
    *   $columnAliaserParam
    * @param connection
    *   $connectionParam
    */
  def source(sql: => Sql, as: ColumnAliaser)(implicit connection: Connection): Source[Row, Future[Int]] =
    source(sql, RowParser.successful, as)

  /** Returns the result rows from the `sql` query as an enumerator. This is equivalent to `source[Row](sql,
    * RowParser.successful, ColumnAliaser.empty)`.
    *
    * $materialization
    *
    * @param sql
    *   $sqlParam
    * @param connection
    *   $connectionParam
    */
  def source(sql: => Sql)(implicit connnection: Connection): Source[Row, Future[Int]] =
    source(sql, RowParser.successful, ColumnAliaser.empty)

  // Internal stages

  import org.apache.pekko.stream.stage.{GraphStageLogic, GraphStageWithMaterializedValue, OutHandler}
  import org.apache.pekko.stream.{Attributes, Outlet, SourceShape}

  import java.sql.ResultSet
  import scala.util.{Failure, Success}

  private[anorm] class ResultSource[T](connection: Connection, sql: Sql, as: ColumnAliaser, parser: RowParser[T])
      extends GraphStageWithMaterializedValue[SourceShape[T], Future[Int]] {

    @SuppressWarnings(Array("org.wartremover.warts.Null"))
    private[anorm] var resultSet: ResultSet = _

    override val toString = "AnormQueryResult"
    val out: Outlet[T] = Outlet(s"${toString}.out")
    val shape: SourceShape[T] = SourceShape(out)

    override def createLogicAndMaterializedValue(inheritedAttributes: Attributes): (GraphStageLogic, Future[Int]) = {
      val result = Promise[Int]()

      val logic = new GraphStageLogic(shape) with OutHandler {
        private var cursor: Option[Cursor] = None
        private var counter: Int = 0

        private def failWith(cause: Throwable): Unit = {
          result.failure(cause)
          fail(out, cause)
          ()
        }

        override def preStart(): Unit = {
          try {
            resultSet = sql.unsafeResultSet(connection)
            nextCursor()
          } catch {
            case NonFatal(cause) => failWith(cause)
          }
        }

        override def postStop() = release()

        private def release(): Unit = {
          val stmt: Option[java.sql.Statement] = {
            if (resultSet != null && !resultSet.isClosed) {
              val s = resultSet.getStatement
              resultSet.close()
              Option(s)
            } else None
          }

          stmt.foreach { s =>
            if (!s.isClosed) s.close()
          }
        }

        private def nextCursor(): Unit = {
          cursor = Sql.unsafeCursor(resultSet, sql.resultSetOnFirstRow, as)
        }

        def onPull(): Unit = cursor match {
          case Some(c) =>
            c.row.as(parser) match {
              case Success(parsed) => {
                counter += 1
                push(out, parsed)
                nextCursor()
              }

              case Failure(cause) =>
                failWith(cause)
            }

          case _ => {
            result.success(counter)
            complete(out)
          }
        }

        @nowarn
        override def onDownstreamFinish() = {
          result.tryFailure(new InterruptedException("Downstream finished"))
          release()
          super.onDownstreamFinish()
        }

        setHandler(out, this)
      }

      logic -> result.future
    }
  }

}


================================================
FILE: server/src/main/scala/controllers/AdminController.scala
================================================
package controllers

import net.wiringbits.api.endpoints.AdminEndpoints
import net.wiringbits.api.models.ErrorResponse
import net.wiringbits.api.models.admin.{AdminGetUserLogs, AdminGetUsers}
import net.wiringbits.common.models.Email
import net.wiringbits.services.AdminService
import org.slf4j.LoggerFactory
import sttp.capabilities.WebSockets
import sttp.capabilities.pekko.PekkoStreams
import sttp.tapir.server.ServerEndpoint

import java.util.UUID
import javax.inject.Inject
import scala.concurrent.{ExecutionContext, Future}

class AdminController @Inject() (
    adminService: AdminService
)(implicit ec: ExecutionContext) {
  private val logger = LoggerFactory.getLogger(this.getClass)

  private def getUserLogs(
      authBasic: String,
      userId: UUID,
      adminCookie: String
  ): Future[Either[ErrorResponse, AdminGetUserLogs.Response]] = handleRequest {
    logger.info(s"Get user logs: $userId")
    for {
      response <- adminService.userLogs(userId)
    } yield Right(response)
  }

  private def getUsers(
      authBasic: String,
      adminCookie: String
  ): Future[Either[ErrorResponse, AdminGetUsers.Response]] = handleRequest {
    logger.info(s"Get users")
    for {
      response <- adminService.users()
      // TODO: Avoid masking data when this the admin website is not public
      maskedResponse = response.copy(data = response.data.map(_.copy(email = Email.trusted("email@wiringbits.net"))))
    } yield Right(maskedResponse)
  }

  def routes: List[ServerEndpoint[PekkoStreams with WebSockets, Future]] = {
    List(
      AdminEndpoints.getUserLogsEndpoint.serverLogic(getUserLogs),
      AdminEndpoints.getUsersEndpoint.serverLogic(getUsers)
    )
  }
}


================================================
FILE: server/src/main/scala/controllers/ApiRouter.scala
================================================
package controllers

import net.wiringbits.api.endpoints.*
import net.wiringbits.config.SwaggerConfig
import org.apache.pekko.actor.ActorSystem
import org.apache.pekko.stream.Materializer
import play.api.routing.Router.Routes
import play.api.routing.SimpleRouter
import sttp.apispec.openapi.Info
import sttp.tapir.AnyEndpoint
import sttp.tapir.server.play.PlayServerInterpreter
import sttp.tapir.swagger.SwaggerUIOptions
import sttp.tapir.swagger.bundle.SwaggerInterpreter

import javax.inject.Inject
import scala.concurrent.{ExecutionContext, Future}

class ApiRouter @Inject() (
    adminController: AdminController,
    authController: AuthController,
    healthController: HealthController,
    usersController: UsersController,
    environmentConfigController: EnvironmentConfigController,
    swaggerConfig: SwaggerConfig
)(using ExecutionContext)
    extends SimpleRouter {
  given ActorSystem = ActorSystem("ApiRouter")

  private val swagger = SwaggerInterpreter(
    swaggerUIOptions = SwaggerUIOptions.default.copy(contextPath = List(swaggerConfig.basePath))
  )
    .fromEndpoints[Future](
      ApiRouter.routes,
      Info(
        title = swaggerConfig.info.title,
        version = swaggerConfig.info.version,
        description = Some(swaggerConfig.info.description)
      )
    )

  override def routes: Routes = PlayServerInterpreter()
    .toRoutes(
      List(
        swagger,
        usersController.routes,
        authController.routes,
        healthController.routes,
        adminController.routes,
        environmentConfigController.routes
      ).flatten
    )
}

object ApiRouter {
  private def routes(implicit ec: ExecutionContext): List[AnyEndpoint] = List(
    HealthEndpoints.routes,
    AdminEndpoints.routes,
    AuthEndpoints.routes,
    UsersEndpoints.routes,
    EnvironmentConfigEndpoints.routes
  ).flatten
}


================================================
FILE: server/src/main/scala/controllers/AuthController.scala
================================================
package controllers

import net.wiringbits.actions.auth.{GetUserAction, LoginAction}
import net.wiringbits.api.endpoints.AuthEndpoints
import net.wiringbits.api.models.*
import net.wiringbits.api.models.auth.{GetCurrentUser, Login, Logout}
import org.slf4j.LoggerFactory
import sttp.capabilities.WebSockets
import sttp.capabilities.pekko.PekkoStreams
import sttp.tapir.server.ServerEndpoint

import java.util.UUID
import javax.inject.Inject
import scala.concurrent.{ExecutionContext, Future}

class AuthController @Inject() (
    loginAction: LoginAction,
    getUserAction: GetUserAction,
    playTapirBridge: PlayTapirBridge
)(implicit ec: ExecutionContext) {
  private val logger = LoggerFactory.getLogger(this.getClass)

  private def login(body: Login.Request): Future[Either[ErrorResponse, (Login.Response, String)]] =
    handleRequest {
      logger.info(s"Login API: ${body.email}")
      for {
        response <- loginAction(body)
        cookieEncoded <- playTapirBridge.setSession(response.id)
      } yield Right(response, cookieEncoded)
    }

  private def me(userIdF: Future[UUID]): Future[Either[ErrorResponse, GetCurrentUser.Response]] =
    handleRequest {
      for {
        userId <- userIdF
        _ = logger.info(s"Get user info: $userId")
        response <- getUserAction(userId)
      } yield Right(response)
    }

  private def logout(userIdF: Future[UUID]): Future[Either[ErrorResponse, (Logout.Response, String)]] =
    handleRequest {
      for {
        _ <- userIdF
        _ = logger.info("Logout")
        header <- playTapirBridge.clearSession()
      } yield Right(Logout.Response(), header)
    }

  def routes: List[ServerEndpoint[PekkoStreams with WebSockets, Future]] = {
    List(
      AuthEndpoints.login.serverLogic(login),
      AuthEndpoints.getCurrentUser.serverLogic(me),
      AuthEndpoints.logout.serverLogic(logout)
    )
  }
}


================================================
FILE: server/src/main/scala/controllers/EnvironmentConfigController.scala
================================================
package controllers

import net.wiringbits.actions.environmentconfig.GetEnvironmentConfigAction
import net.wiringbits.api.endpoints.EnvironmentConfigEndpoints
import net.wiringbits.api.models.ErrorResponse
import net.wiringbits.api.models.environmentconfig.GetEnvironmentConfig
import org.slf4j.LoggerFactory
import sttp.capabilities.WebSockets
import sttp.capabilities.pekko.PekkoStreams
import sttp.tapir.server.ServerEndpoint

import javax.inject.Inject
import scala.concurrent.{ExecutionContext, Future}

class EnvironmentConfigController @Inject() (
    getEnvironmentConfigAction: GetEnvironmentConfigAction
)(implicit ec: ExecutionContext) {
  private val logger = LoggerFactory.getLogger(this.getClass)

  private def getEnvironmentConfig: Future[Either[ErrorResponse, GetEnvironmentConfig.Response]] = handleRequest {
    logger.info("Get frontend config")
    for {
      response <- getEnvironmentConfigAction()
    } yield Right(response)
  }

  def routes: List[ServerEndpoint[PekkoStreams with WebSockets, Future]] = {
    List(EnvironmentConfigEndpoints.getEnvironmentConfig.serverLogic(_ => getEnvironmentConfig))
  }
}


================================================
FILE: server/src/main/scala/controllers/HealthController.scala
================================================
package controllers

import net.wiringbits.api.endpoints.HealthEndpoints
import sttp.capabilities.WebSockets
import sttp.capabilities.pekko.PekkoStreams
import sttp.model.headers.{Cookie, CookieValueWithMeta, CookieWithMeta}
import sttp.tapir.server.ServerEndpoint

import java.time.Instant
import java.time.temporal.ChronoUnit
import javax.inject.Inject
import scala.concurrent.{ExecutionContext, Future}

class HealthController @Inject() (implicit ec: ExecutionContext) {
  private def check: Future[Either[Unit, Unit]] =
    Future.successful(Right(()))

  def routes: List[ServerEndpoint[PekkoStreams with WebSockets, Future]] = {
    List(HealthEndpoints.check.serverLogic(_ => check))
  }
}


================================================
FILE: server/src/main/scala/controllers/UsersController.scala
================================================
package controllers

import net.wiringbits.actions.*
import net.wiringbits.actions.users.*
import net.wiringbits.api.endpoints.UsersEndpoints
import net.wiringbits.api.models.*
import net.wiringbits.api.models.users.*
import org.slf4j.LoggerFactory
import sttp.capabilities.WebSockets
import sttp.capabilities.pekko.PekkoStreams
import sttp.tapir.server.ServerEndpoint

import java.util.UUID
import javax.inject.Inject
import scala.concurrent.{ExecutionContext, Future}

class UsersController @Inject() (
    createUserAction: CreateUserAction,
    verifyUserEmailAction: VerifyUserEmailAction,
    forgotPasswordAction: ForgotPasswordAction,
    resetPasswordAction: ResetPasswordAction,
    updateUserAction: UpdateUserAction,
    updatePasswordAction: UpdatePasswordAction,
    getUserLogsAction: GetUserLogsAction,
    sendEmailVerificationTokenAction: SendEmailVerificationTokenAction
)(implicit ec: ExecutionContext) {
  private val logger = LoggerFactory.getLogger(this.getClass)

  private def create(request: CreateUser.Request): Future[Either[ErrorResponse, CreateUser.Response]] = handleRequest {
    logger.info(s"Create user: ${request.email.string}")
    for {
      response <- createUserAction(request)
    } yield Right(response)
  }

  private def verifyEmail(request: VerifyEmail.Request) = handleRequest {
    val token = request.token
    logger.info(s"Verify user's email: ${token.userId}")
    for {
      response <- verifyUserEmailAction(token.userId, token.token)
    } yield Right(response)
  }

  private def forgotPassword(request: ForgotPassword.Request): Future[Either[ErrorResponse, ForgotPassword.Response]] =
    handleRequest {
      logger.info(s"Send a link to reset password for user with email: ${request.email}")
      for {
        response <- forgotPasswordAction(request)
      } yield Right(response)
    }

  private def resetPassword(request: ResetPassword.Request): Future[Either[ErrorResponse, ResetPassword.Response]] =
    handleRequest {
      logger.info(s"Reset user's password: ${request.token.userId}")
      for {
        response <- resetPasswordAction(request.token.userId, request.token.token, request.password)
      } yield Right(response)
    }

  private def sendEmailVerificationToken(
      request: SendEmailVerificationToken.Request
  ): Future[Either[ErrorResponse, SendEmailVerificationToken.Response]] =
    handleRequest {
      logger.info(s"Send email to: ${request.email}")
      for {
        response <- sendEmailVerificationTokenAction(request)
      } yield Right(response)
    }

  private def update(
      request: UpdateUser.Request,
      userIdF: Future[UUID]
  ): Future[Either[ErrorResponse, UpdateUser.Response]] = handleRequest {
    logger.info(s"Update user: $request")
    for {
      userId <- userIdF
      _ <- updateUserAction(userId, request)
      response = UpdateUser.Response()
    } yield Right(response)
  }

  private def updatePassword(
      request: UpdatePassword.Request,
      userIdF: Future[UUID]
  ): Future[Either[ErrorResponse, UpdatePassword.Response]] = handleRequest {
    for {
      userId <- userIdF
      _ = logger.info(s"Update password for: $userId")
      _ <- updatePasswordAction(userId, request)
      response = UpdatePassword.Response()
    } yield Right(response)
  }

  private def getLogs(userIdF: Future[UUID]): Future[Either[ErrorResponse, GetUserLogs.Response]] =
    handleRequest {
      for {
        userId <- userIdF
        _ = logger.info(s"Get user logs: $userId")
        response <- getUserLogsAction(userId)
      } yield Right(response)
    }

  def routes: List[ServerEndpoint[PekkoStreams with WebSockets, Future]] = {
    List(
      UsersEndpoints.create.serverLogic(create),
      UsersEndpoints.verifyEmail.serverLogic(verifyEmail),
      UsersEndpoints.forgotPassword.serverLogic(forgotPassword),
      UsersEndpoints.resetPassword.serverLogic(resetPassword),
      UsersEndpoints.sendEmailVerificationToken.serverLogic(sendEmailVerificationToken),
      UsersEndpoints.update.serverLogic(update),
      UsersEndpoints.updatePassword.serverLogic(updatePassword),
      UsersEndpoints.getLogs.serverLogic(getLogs)
    )
  }
}


================================================
FILE: server/src/main/scala/controllers/package.scala
================================================
import net.wiringbits.api.models.{ErrorResponse, errorResponseFormat}
import org.slf4j.LoggerFactory
import play.api.mvc.request.DefaultRequestFactory
import play.api.mvc.{CookieHeaderEncoding, RequestHeader, Session}
import sttp.tapir.model.ServerRequest

import java.util.UUID
import javax.inject.Inject
import scala.concurrent.{ExecutionContext, Future}
import scala.language.implicitConversions
import scala.util.Try
import scala.util.control.NonFatal

package object controllers {
  private val logger = LoggerFactory.getLogger(this.getClass)

  class PlayTapirBridge @Inject() (
      requestFactory: DefaultRequestFactory,
      cookieHeaderEncoding: CookieHeaderEncoding
  )(implicit ec: ExecutionContext) {
    def setSession(userId: UUID): Future[String] = Future {
      val session = Session(Map("id" -> userId.toString))
      val playCookie = requestFactory.sessionBaker.encodeAsCookie(session)
      cookieHeaderEncoding.encodeSetCookieHeader(List(playCookie))
    }

    def clearSession(): Future[String] = Future {
      val encoded = requestFactory.sessionBaker.discard.toCookie
      cookieHeaderEncoding.encodeSetCookieHeader(List(encoded))
    }
  }

  def handleRequest[R](
      block: Future[Right[ErrorResponse, R]]
  )(implicit ec: ExecutionContext): Future[Either[ErrorResponse, R]] = {
    block.recover(errorHandler)
  }

  def errorHandler[R]: PartialFunction[Throwable, Left[ErrorResponse, R]] = {
    // rendering any error this way should be enough for a while
    case NonFatal(ex) =>
      // debug level used because this includes any validation error as well as server errors
      logger.debug(s"Error response while handling a request: ${ex.getMessage}", ex)
      Left(ErrorResponse(ex.getMessage))
  }

  // This is the way to access the play request from tapir, we need it to extract the play session
  // UUID has to be future, because we want to handle the exception in the controllers
  implicit def authHandler(serverRequest: ServerRequest)(implicit ec: ExecutionContext): Future[UUID] =
    val session = serverRequest.underlying
      .asInstanceOf[RequestHeader]
      .session

    def userIdFromSession = Future {
      session
        .get("id")
        .flatMap(str => Try(UUID.fromString(str)).toOption)
        .getOrElse(throw new RuntimeException("Invalid or missing authentication"))
    }

    userIdFromSession
      .recover { case NonFatal(_) =>
        throw new RuntimeException("Unauthorized: Invalid or missing authentication")
      }
}


================================================
FILE: server/src/main/scala/net/wiringbits/actions/auth/GetUserAction.scala
================================================
package net.wiringbits.actions.auth

import io.scalaland.chimney.dsl.transformInto
import net.wiringbits.api.models.auth.GetCurrentUser
import net.wiringbits.repositories.UsersRepository
import net.wiringbits.repositories.models.User

import java.util.UUID
import javax.inject.Inject
import scala.concurrent.{ExecutionContext, Future}

class GetUserAction @Inject() (
    usersRepository: UsersRepository
)(implicit ec: ExecutionContext) {

  def apply(userId: UUID): Future[GetCurrentUser.Response] = {
    for {
      user <- unsafeUser(userId)
    } yield user.transformInto[GetCurrentUser.Response]
  }

  private def unsafeUser(userId: UUID): Future[User] = {
    usersRepository
      .find(userId)
      .map { maybe =>
        maybe.getOrElse(
          throw new RuntimeException(
            s"Unexpected error because the user wasn't found: $userId"
          )
        )
      }
  }
}


================================================
FILE: server/src/main/scala/net/wiringbits/actions/auth/LoginAction.scala
================================================
package net.wiringbits.actions.auth

import io.scalaland.chimney.dsl.transformInto
import net.wiringbits.api.models.auth.Login
import net.wiringbits.apis.ReCaptchaApi
import net.wiringbits.repositories.{UserLogsRepository, UsersRepository}
import net.wiringbits.validations.{ValidateCaptcha, ValidatePasswordMatches, ValidateVerifiedUser}

import javax.inject.Inject
import scala.concurrent.{ExecutionContext, Future}

class LoginAction @Inject() (
    captchaApi: ReCaptchaApi,
    usersRepository: UsersRepository,
    userLogsRepository: UserLogsRepository
)(implicit
    ec: ExecutionContext
) {
  // returns the token to use for authenticating requests
  def apply(request: Login.Request): Future[Login.Response] = {
    for {
      _ <- ValidateCaptcha(captchaApi, request.captcha)
      // the user is verified
      maybe <- usersRepository.find(request.email)
      _ = maybe.foreach(ValidateVerifiedUser.apply)

      // The password matches
      user = ValidatePasswordMatches(maybe, request.password)

      // A login token is created
      _ <- userLogsRepository.create(user.id, "Logged in successfully")
    } yield user.transformInto[Login.Response]
  }
}


================================================
FILE: server/src/main/scala/net/wiringbits/actions/environmentconfig/GetEnvironmentConfigAction.scala
================================================
package net.wiringbits.actions.environmentconfig

import net.wiringbits.api.models.environmentconfig.GetEnvironmentConfig
import net.wiringbits.config.ReCaptchaConfig

import javax.inject.Inject
import scala.concurrent.Future

class GetEnvironmentConfigAction @Inject() (
    reCaptchaConfig: ReCaptchaConfig
)() {
  def apply(): Future[GetEnvironmentConfig.Response] = Future.successful {
    GetEnvironmentConfig.Response(reCaptchaConfig.siteKey.string)
  }
}


================================================
FILE: server/src/main/scala/net/wiringbits/actions/internal/StreamPendingBackgroundJobsForeverAction.scala
================================================
package net.wiringbits.actions.internal

import org.apache.pekko.actor.ActorSystem
import org.apache.pekko.stream.scaladsl.*
import net.wiringbits.repositories.BackgroundJobsRepository
import net.wiringbits.repositories.models.BackgroundJobData
import org.slf4j.LoggerFactory

import javax.inject.Inject
import scala.concurrent.duration.{DurationInt, FiniteDuration}
import scala.concurrent.{ExecutionContext, Future}

class StreamPendingBackgroundJobsForeverAction @Inject() (backgroundJobsRepository: BackgroundJobsRepository)(implicit
    ec: ExecutionContext,
    system: ActorSystem
) {
  private val logger = LoggerFactory.getLogger(this.getClass)

  def apply(reconnectionDelay: FiniteDuration = 10.seconds): Source[BackgroundJobData, org.apache.pekko.NotUsed] = {
    // Let's use unfoldAsync to continuously fetch items from database
    // First execution doesn't involve a delay
    Source
      .unfoldAsync[Boolean, Source[BackgroundJobData, Future[Int]]](false) { delay =>
        logger.trace(s"Looking for pending background jobs")
        org.apache.pekko.pattern
          .after(if (delay) reconnectionDelay else 0.seconds) {
            backgroundJobsRepository.streamPendingJobs
          }
          .map(source => Some(true -> source))
      }
      .flatMapConcat(identity)
  }
}


================================================
FILE: server/src/main/scala/net/wiringbits/actions/users/CreateUserAction.scala
================================================
package net.wiringbits.actions.users

import io.scalaland.chimney.dsl.transformInto
import net.wiringbits.api.models.users.CreateUser
import net.wiringbits.apis.ReCaptchaApi
import net.wiringbits.config.UserTokensConfig
import net.wiringbits.repositories
import net.wiringbits.repositories.UsersRepository
import net.wiringbits.repositories.models.User
import net.wiringbits.util.{EmailsHelper, TokenGenerator, TokensHelper}
import net.wiringbits.validations.{ValidateCaptcha, ValidateEmailIsAvailable}
import org.mindrot.jbcrypt.BCrypt

import java.time.Instant
import java.util.UUID
import javax.inject.Inject
import scala.concurrent.{ExecutionContext, Future}

class CreateUserAction @Inject() (
    usersRepository: UsersRepository,
    reCaptchaApi: ReCaptchaApi,
    tokenGenerator: TokenGenerator,
    userTokensConfig: UserTokensConfig,
    emailsHelper: EmailsHelper
)(implicit
    ec: ExecutionContext
) {

  def apply(request: CreateUser.Request): Future[CreateUser.Response] = {
    for {
      _ <- validations(request)
      hashedPassword = BCrypt.hashpw(request.password.string, BCrypt.gensalt())
      token = tokenGenerator.next()
      hmacToken = TokensHelper.doHMACSHA1(token.toString.getBytes(), userTokensConfig.hmacSecret)

      // create the user
      createUser = repositories.models.User
        .CreateUser(
          id = UUID.randomUUID(),
          name = request.name,
          email = request.email,
          hashedPassword = hashedPassword,
          verifyEmailToken = hmacToken
        )
      _ <- usersRepository.create(createUser)

      // then, send the verification email
      _ <- emailsHelper.sendRegistrationEmailWithVerificationToken(
        User(
          id = createUser.id,
          name = request.name,
          email = request.email,
          hashedPassword = hashedPassword,
          createdAt = Instant.now,
          verifiedOn = None
        ),
        token
      )
    } yield createUser.transformInto[CreateUser.Response]
  }

  private def validations(request: CreateUser.Request) = {
    for {
      _ <- ValidateCaptcha(reCaptchaApi, request.captcha)
      _ <- ValidateEmailIsAvailable(usersRepository, request.email)
    } yield ()
  }
}


================================================
FILE: server/src/main/scala/net/wiringbits/actions/users/ForgotPasswordAction.scala
================================================
package net.wiringbits.actions.users

import net.wiringbits.api.models.users.ForgotPassword
import net.wiringbits.apis.ReCaptchaApi
import net.wiringbits.repositories.UsersRepository
import net.wiringbits.repositories.models.User
import net.wiringbits.util.EmailsHelper
import net.wiringbits.validations.{ValidateCaptcha, ValidateVerifiedUser}

import javax.inject.Inject
import scala.concurrent.{ExecutionContext, Future}

class ForgotPasswordAction @Inject() (
    captchaApi: ReCaptchaApi,
    usersRepository: UsersRepository,
    emailsHelper: EmailsHelper
)(implicit ec: ExecutionContext) {
  def apply(request: ForgotPassword.Request): Future[ForgotPassword.Response] = {
    for {
      _ <- ValidateCaptcha(captchaApi, request.captcha)
      userMaybe <- usersRepository.find(request.email)

      // submit the email only when the user exists, otherwise, ignore the request
      _ <- userMaybe.map(whenExists).getOrElse(Future.unit)
    } yield ForgotPassword.Response()
  }

  private def whenExists(user: User) = {
    for {
      _ <- Future { ValidateVerifiedUser(user) }
      _ <- emailsHelper.sendPasswordRecoveryEmail(user)
    } yield ()
  }
}


================================================
FILE: server/src/main/scala/net/wiringbits/actions/users/GetUserLogsAction.scala
================================================
package net.wiringbits.actions.users

import io.scalaland.chimney.dsl.transformInto
import net.wiringbits.api.models.users.GetUserLogs
import net.wiringbits.repositories.UserLogsRepository

import java.util.UUID
import javax.inject.Inject
import scala.concurrent.{ExecutionContext, Future}

class GetUserLogsAction @Inject() (
    userLogsRepository: UserLogsRepository
)(implicit ec: ExecutionContext) {

  def apply(userId: UUID): Future[GetUserLogs.Response] = {
    for {
      logs <- userLogsRepository.logs(userId)
      items = logs.map(_.transformInto[GetUserLogs.Response.UserLog])
    } yield GetUserLogs.Response(items)
  }
}


================================================
FILE: server/src/main/scala/net/wiringbits/actions/users/ResetPasswordAction.scala
================================================
package net.wiringbits.actions.users

import net.wiringbits.api.models.users.ResetPassword
import net.wiringbits.common.models.Password
import net.wiringbits.config.UserTokensConfig
import net.wiringbits.repositories.{UserTokensRepository, UsersRepository}
import net.wiringbits.util.{EmailMessage, TokensHelper}
import net.wiringbits.validations.ValidateUserToken
import org.mindrot.jbcrypt.BCrypt

import java.time.Clock
import java.util.UUID
import javax.inject.Inject
import scala.concurrent.{ExecutionContext, Future}

class ResetPasswordAction @Inject() (
    userTokensConfig: UserTokensConfig,
    usersRepository: UsersRepository,
    userTokensRepository: UserTokensRepository
)(implicit
    ec: ExecutionContext,
    clock: Clock
) {

  def apply(userId: UUID, token: UUID, password: Password): Future[ResetPassword.Response] = {
    val hashedPassword = BCrypt.hashpw(password.string, BCrypt.gensalt())
    val hmacToken = TokensHelper.doHMACSHA1(token.toString.getBytes, userTokensConfig.hmacSecret)
    for {
      // When the token valid
      tokenMaybe <- userTokensRepository.find(userId, hmacToken)
      token = tokenMaybe.getOrElse(throw new RuntimeException(s"Token for user $userId wasn't found"))
      _ = ValidateUserToken(token)

      // We trigger the reset password flow
      userMaybe <- usersRepository.find(userId)
      user = userMaybe.getOrElse(throw new RuntimeException(s"User with id $userId wasn't found"))
      emailMessage = EmailMessage.resetPassword(user.name)
      _ <- usersRepository.resetPassword(userId, hashedPassword, emailMessage)
    } yield ResetPassword.Response(name = user.name, email = user.email)
  }
}


================================================
FILE: server/src/main/scala/net/wiringbits/actions/users/SendEmailVerificationTokenAction.scala
================================================
package net.wiringbits.actions.users

import net.wiringbits.api.models.users.SendEmailVerificationToken
import net.wiringbits.apis.ReCaptchaApi
import net.wiringbits.repositories.UsersRepository
import net.wiringbits.util.EmailsHelper
import net.wiringbits.validations.{ValidateCaptcha, ValidateEmailIsRegistered, ValidateUserIsNotVerified}

import javax.inject.Inject
import scala.concurrent.{ExecutionContext, Future}

class SendEmailVerificationTokenAction @Inject() (
    usersRepository: UsersRepository,
    emailsHelper: EmailsHelper,
    reCaptchaApi: ReCaptchaApi
)(implicit ec: ExecutionContext) {

  def apply(request: SendEmailVerificationToken.Request): Future[SendEmailVerificationToken.Response] = {
    for {
      _ <- validations(request)
      userMaybe <- usersRepository.find(request.email)
      user = userMaybe.getOrElse(throw new RuntimeException(s"User with email ${request.email} wasn't found"))
      _ = ValidateUserIsNotVerified(user)

      expiresAt <- emailsHelper.sendEmailVerificationToken(user)
    } yield SendEmailVerificationToken.Response(expiresAt = expiresAt)
  }

  private def validations(request: SendEmailVerificationToken.Request) = {
    for {
      _ <- ValidateCaptcha(reCaptchaApi, request.captcha)
      _ <- ValidateEmailIsRegistered(usersRepository, request.email)
    } yield ()
  }
}


================================================
FILE: server/src/main/scala/net/wiringbits/actions/users/UpdatePasswordAction.scala
================================================
package net.wiringbits.actions.users

import net.wiringbits.api.models.users.UpdatePassword
import net.wiringbits.repositories.UsersRepository
import net.wiringbits.util.EmailMessage
import net.wiringbits.validations.ValidatePasswordMatches
import org.mindrot.jbcrypt.BCrypt

import java.util.UUID
import javax.inject.Inject
import scala.concurrent.{ExecutionContext, Future}

class UpdatePasswordAction @Inject() (
    usersRepository: UsersRepository
)(implicit ec: ExecutionContext) {

  def apply(userId: UUID, request: UpdatePassword.Request): Future[Unit] = {
    for {
      maybe <- usersRepository.find(userId)
      user = ValidatePasswordMatches(maybe, request.oldPassword)
      hashedPassword = BCrypt.hashpw(request.newPassword.string, BCrypt.gensalt())
      emailMessage = EmailMessage.updatePassword(user.name)
      _ <- usersRepository.updatePassword(userId, hashedPassword, emailMessage)
    } yield ()
  }
}


================================================
FILE: server/src/main/scala/net/wiringbits/actions/users/UpdateUserAction.scala
================================================
package net.wiringbits.actions.users

import net.wiringbits.api.models.users.UpdateUser
import net.wiringbits.repositories.UsersRepository

import java.util.UUID
import javax.inject.Inject
import scala.concurrent.{ExecutionContext, Future}

class UpdateUserAction @Inject() (
    usersRepository: UsersRepository
)(implicit ec: ExecutionContext) {

  def apply(userId: UUID, request: UpdateUser.Request): Future[Unit] = {
    val validate = Future {
      if (request.name.string.isEmpty) new RuntimeException(s"The name is required")
      else ()
    }

    for {
      _ <- validate
      _ <- usersRepository.update(userId, request.name)
    } yield ()
  }
}


================================================
FILE: server/src/main/scala/net/wiringbits/actions/users/VerifyUserEmailAction.scala
================================================
package net.wiringbits.actions.users

import net.wiringbits.api.models.users.VerifyEmail
import net.wiringbits.config.UserTokensConfig
import net.wiringbits.repositories.{UserTokensRepository, UsersRepository}
import net.wiringbits.util.{EmailMessage, TokensHelper}
import net.wiringbits.validations.{ValidateUserIsNotVerified, ValidateUserToken}

import java.time.Clock
import java.util.UUID
import javax.inject.Inject
import scala.concurrent.{ExecutionContext, Future}

class VerifyUserEmailAction @Inject() (
    usersRepository: UsersRepository,
    userTokensRepository: UserTokensRepository,
    userTokensConfig: UserTokensConfig
)(implicit
    ec: ExecutionContext,
    clock: Clock
) {
  def apply(userId: UUID, token: UUID): Future[VerifyEmail.Response] = for {
    // when the user is not verified
    userMaybe <- usersRepository.find(userId)
    user = userMaybe.getOrElse(throw new RuntimeException(s"User wasn't found"))
    _ = ValidateUserIsNotVerified(user)

    // the 
Download .txt
gitextract_jae30_9x/

├── .github/
│   └── workflows/
│       ├── codepreview.yml
│       └── pull_request.yml
├── .gitignore
├── .nvmrc
├── .sbtopts
├── .scalafmt.conf
├── .sdkmanrc
├── LICENSE
├── README.md
├── build.sbt
├── custom.webpack.config.js
├── docs/
│   ├── README.md
│   ├── architecture.md
│   ├── design-decisions.md
│   ├── diagram-sources/
│   │   ├── architecture-infra.puml
│   │   ├── architecture-modules.puml
│   │   ├── architecture-server-actions.puml
│   │   ├── architecture-server-controllers.puml
│   │   ├── architecture-server-daos.puml
│   │   ├── architecture-server-external-apis.puml
│   │   ├── architecture-server-repositories.puml
│   │   ├── architecture-server-services.puml
│   │   └── architecture-server.puml
│   ├── learning-material.md
│   ├── setup-dev-environment.md
│   └── swagger-integration.md
├── infra/
│   ├── .gitignore
│   ├── README.md
│   ├── admin.yml
│   ├── config/
│   │   ├── nginx/
│   │   │   ├── admin-app-htpasswd
│   │   │   ├── admin_app_site.j2
│   │   │   ├── mime.types
│   │   │   ├── nginx.conf
│   │   │   ├── preview_admin_app_site.j2
│   │   │   ├── preview_web_app_site.j2
│   │   │   └── web_app_site.j2
│   │   └── server/
│   │       ├── dev.env.j2
│   │       └── server.service.j2
│   ├── demo-hosts.ini
│   ├── nginx.yml
│   ├── nginx_site_admin.yml
│   ├── nginx_site_web.yml
│   ├── preview_nginx_site_admin.yml
│   ├── preview_nginx_site_web.yml
│   ├── scripts/
│   │   ├── build-admin.sh
│   │   ├── build-server.sh
│   │   └── build-web.sh
│   ├── server.yml
│   ├── setup-postgres.md
│   ├── test-hosts.ini
│   └── web.yml
├── lib/
│   ├── api/
│   │   └── shared/
│   │       └── src/
│   │           └── main/
│   │               └── scala/
│   │                   └── net/
│   │                       └── wiringbits/
│   │                           └── api/
│   │                               ├── ApiClient.scala
│   │                               ├── endpoints/
│   │                               │   ├── AdminEndpoints.scala
│   │                               │   ├── AuthEndpoints.scala
│   │                               │   ├── EnvironmentConfigEndpoints.scala
│   │                               │   ├── HealthEndpoints.scala
│   │                               │   ├── UsersEndpoints.scala
│   │                               │   └── package.scala
│   │                               ├── models/
│   │                               │   ├── PlayErrorResponse.scala
│   │                               │   ├── admin/
│   │                               │   │   ├── AdminGetUserLogs.scala
│   │                               │   │   └── AdminGetUsers.scala
│   │                               │   ├── auth/
│   │                               │   │   ├── GetCurrentUser.scala
│   │                               │   │   ├── Login.scala
│   │                               │   │   └── Logout.scala
│   │                               │   ├── environmentconfig/
│   │                               │   │   └── GetEnvironmentConfig.scala
│   │                               │   ├── package.scala
│   │                               │   └── users/
│   │                               │       ├── CreateUser.scala
│   │                               │       ├── ForgotPassword.scala
│   │                               │       ├── GetUserLogs.scala
│   │                               │       ├── ResetPassword.scala
│   │                               │       ├── SendEmailVerificationToken.scala
│   │                               │       ├── UpdatePassword.scala
│   │                               │       ├── UpdateUser.scala
│   │                               │       └── VerifyEmail.scala
│   │                               └── utils/
│   │                                   └── Formatter.scala
│   ├── common/
│   │   ├── js/
│   │   │   └── src/
│   │   │       └── test/
│   │   │           └── scala/
│   │   │               └── java/
│   │   │                   └── security/
│   │   │                       └── SecureRandom.scala
│   │   └── shared/
│   │       └── src/
│   │           ├── main/
│   │           │   └── scala/
│   │           │       └── net/
│   │           │           └── wiringbits/
│   │           │               └── common/
│   │           │                   ├── ErrorMessages.scala
│   │           │                   └── models/
│   │           │                       ├── Captcha.scala
│   │           │                       ├── Email.scala
│   │           │                       ├── Name.scala
│   │           │                       ├── Password.scala
│   │           │                       └── UserToken.scala
│   │           └── test/
│   │               └── scala/
│   │                   └── net/
│   │                       └── wiringbits/
│   │                           └── common/
│   │                               └── models/
│   │                                   ├── EmailSpec.scala
│   │                                   ├── NameSpec.scala
│   │                                   ├── PasswordSpec.scala
│   │                                   └── UserTokenSpec.scala
│   └── ui/
│       └── src/
│           └── main/
│               └── scala/
│                   └── net/
│                       └── wiringbits/
│                           └── ui/
│                               └── components/
│                                   ├── core/
│                                   │   └── widgets/
│                                   │       └── ValidatedTextInput.scala
│                                   └── inputs/
│                                       └── inputs.scala
├── project/
│   ├── build.properties
│   └── plugins.sbt
├── server/
│   └── src/
│       ├── main/
│       │   ├── resources/
│       │   │   ├── application.conf
│       │   │   ├── evolutions/
│       │   │   │   └── default/
│       │   │   │       ├── 1.sql
│       │   │   │       └── 2.sql
│       │   │   ├── logback.xml
│       │   │   ├── messages
│       │   │   └── routes
│       │   └── scala/
│       │       ├── PekkoStream.scala
│       │       ├── controllers/
│       │       │   ├── AdminController.scala
│       │       │   ├── ApiRouter.scala
│       │       │   ├── AuthController.scala
│       │       │   ├── EnvironmentConfigController.scala
│       │       │   ├── HealthController.scala
│       │       │   ├── UsersController.scala
│       │       │   └── package.scala
│       │       └── net/
│       │           └── wiringbits/
│       │               ├── actions/
│       │               │   ├── auth/
│       │               │   │   ├── GetUserAction.scala
│       │               │   │   └── LoginAction.scala
│       │               │   ├── environmentconfig/
│       │               │   │   └── GetEnvironmentConfigAction.scala
│       │               │   ├── internal/
│       │               │   │   └── StreamPendingBackgroundJobsForeverAction.scala
│       │               │   └── users/
│       │               │       ├── CreateUserAction.scala
│       │               │       ├── ForgotPasswordAction.scala
│       │               │       ├── GetUserLogsAction.scala
│       │               │       ├── ResetPasswordAction.scala
│       │               │       ├── SendEmailVerificationTokenAction.scala
│       │               │       ├── UpdatePasswordAction.scala
│       │               │       ├── UpdateUserAction.scala
│       │               │       └── VerifyUserEmailAction.scala
│       │               ├── apis/
│       │               │   ├── EmailApi.scala
│       │               │   ├── EmailApiAWSImpl.scala
│       │               │   ├── ReCaptchaApi.scala
│       │               │   └── models/
│       │               │       └── EmailRequest.scala
│       │               ├── config/
│       │               │   ├── AWSConfig.scala
│       │               │   ├── BackgroundJobsExecutorConfig.scala
│       │               │   ├── EmailConfig.scala
│       │               │   ├── ReCaptchaConfig.scala
│       │               │   ├── SwaggerConfig.scala
│       │               │   ├── UserTokensConfig.scala
│       │               │   └── WebAppConfig.scala
│       │               ├── executors/
│       │               │   └── DatabaseExecutionContext.scala
│       │               ├── models/
│       │               │   ├── AWSAccessKeyId.scala
│       │               │   ├── AWSSecretAccessKey.scala
│       │               │   ├── ReCaptchaSecret.scala
│       │               │   ├── ReCaptchaSiteKey.scala
│       │               │   ├── SecretValue.scala
│       │               │   └── jobs/
│       │               │       ├── BackgroundJobPayload.scala
│       │               │       ├── BackgroundJobStatus.scala
│       │               │       └── BackgroundJobType.scala
│       │               ├── modules/
│       │               │   ├── ApisModule.scala
│       │               │   ├── ClockModule.scala
│       │               │   ├── ConfigModule.scala
│       │               │   ├── ExecutorsModule.scala
│       │               │   └── TasksModule.scala
│       │               ├── repositories/
│       │               │   ├── BackgroundJobsRepository.scala
│       │               │   ├── UserLogsRepository.scala
│       │               │   ├── UserTokensRepository.scala
│       │               │   ├── UsersRepository.scala
│       │               │   ├── daos/
│       │               │   │   ├── BackgroundJobDAO.scala
│       │               │   │   ├── UserLogsDAO.scala
│       │               │   │   ├── UserTokensDAO.scala
│       │               │   │   ├── UsersDAO.scala
│       │               │   │   └── package.scala
│       │               │   └── models/
│       │               │       ├── BackgroundJobData.scala
│       │               │       ├── User.scala
│       │               │       ├── UserLog.scala
│       │               │       ├── UserToken.scala
│       │               │       └── UserTokenType.scala
│       │               ├── services/
│       │               │   └── AdminService.scala
│       │               ├── tasks/
│       │               │   └── BackgroundJobsExecutorTask.scala
│       │               ├── util/
│       │               │   ├── DelayGenerator.scala
│       │               │   ├── EmailMessage.scala
│       │               │   ├── EmailsHelper.scala
│       │               │   ├── StringUtils.scala
│       │               │   ├── TokenGenerator.scala
│       │               │   └── TokensHelper.scala
│       │               └── validations/
│       │                   ├── ValidateCaptcha.scala
│       │                   ├── ValidateEmailIsAvailable.scala
│       │                   ├── ValidateEmailIsRegistered.scala
│       │                   ├── ValidatePasswordMatches.scala
│       │                   ├── ValidateUserIsNotVerified.scala
│       │                   ├── ValidateUserToken.scala
│       │                   └── ValidateVerifiedUser.scala
│       └── test/
│           └── scala/
│               ├── controllers/
│               │   ├── AdminControllerSpec.scala
│               │   ├── AuthControllerSpec.scala
│               │   ├── EnvironmentConfigControllerSpec.scala
│               │   ├── UsersControllerSpec.scala
│               │   └── common/
│               │       ├── PlayAPISpec.scala
│               │       └── PlayPostgresSpec.scala
│               ├── net/
│               │   └── wiringbits/
│               │       ├── apis/
│               │       │   └── ReCaptchaApiSpec.scala
│               │       ├── core/
│               │       │   ├── PostgresSpec.scala
│               │       │   ├── RepositoryComponents.scala
│               │       │   └── RepositorySpec.scala
│               │       ├── repositories/
│               │       │   ├── BackgroundJobsRepositorySpec.scala
│               │       │   ├── UserLogsRepositorySpec.scala
│               │       │   ├── UserTokensRepositorySpec.scala
│               │       │   └── UsersRepositorySpec.scala
│               │       └── util/
│               │           ├── DelayGeneratorSpec.scala
│               │           └── TokensHelperSpec.scala
│               └── utils/
│                   ├── Executors.scala
│                   ├── LoginUtils.scala
│                   └── RepositoryUtils.scala
└── web/
    └── src/
        ├── main/
        │   ├── js/
        │   │   ├── index.css
        │   │   └── index.html
        │   └── scala/
        │       └── net/
        │           └── wiringbits/
        │               ├── API.scala
        │               ├── App.scala
        │               ├── AppContext.scala
        │               ├── AppRouter.scala
        │               ├── AppTheme.scala
        │               ├── I18nMessages.scala
        │               ├── Main.scala
        │               ├── components/
        │               │   ├── AppSplash.scala
        │               │   ├── pages/
        │               │   │   ├── AboutPage.scala
        │               │   │   ├── DashboardPage.scala
        │               │   │   ├── ForgotPasswordPage.scala
        │               │   │   ├── HomePage.scala
        │               │   │   ├── ResendVerifyEmailPage.scala
        │               │   │   ├── ResetPasswordPage.scala
        │               │   │   ├── SignInPage.scala
        │               │   │   ├── SignUpPage.scala
        │               │   │   ├── UserEditPage.scala
        │               │   │   ├── VerifyEmailPage.scala
        │               │   │   └── VerifyEmailWithTokenPage.scala
        │               │   └── widgets/
        │               │       ├── AppBar.scala
        │               │       ├── AppCard.scala
        │               │       ├── EditPasswordForm.scala
        │               │       ├── EditUserForm.scala
        │               │       ├── Footer.scala
        │               │       ├── ForgotPasswordForm.scala
        │               │       ├── Loader.scala
        │               │       ├── LogList.scala
        │               │       ├── Logs.scala
        │               │       ├── ReCaptcha.scala
        │               │       ├── ResendVerifyEmailForm.scala
        │               │       ├── ResetPasswordForm.scala
        │               │       ├── SignInForm.scala
        │               │       ├── SignUpForm.scala
        │               │       └── UserInfo.scala
        │               ├── core/
        │               │   ├── I18nHooks.scala
        │               │   ├── I18nLang.scala
        │               │   └── ReactiveHooks.scala
        │               ├── forms/
        │               │   ├── ForgotPasswordFormData.scala
        │               │   ├── ResendVerifyEmailFormData.scala
        │               │   ├── ResetPasswordFormData.scala
        │               │   ├── SignInFormData.scala
        │               │   ├── SignUpFormData.scala
        │               │   ├── UpdateInfoFormData.scala
        │               │   └── UpdatePasswordFormData.scala
        │               ├── models/
        │               │   ├── AuthState.scala
        │               │   ├── User.scala
        │               │   └── UserMenuOption.scala
        │               └── services/
        │                   └── StorageService.scala
        └── test/
            └── scala/
                ├── java/
                │   └── security/
                │       └── SecureRandom.scala
                └── net/
                    └── wiringbits/
                        └── forms/
                            ├── ForgotPasswordFormDataSpec.scala
                            ├── ResendVerifyEmailFormDataSpec.scala
                            ├── ResetPasswordFormDataSpec.scala
                            ├── SignInFormDataSpec.scala
                            ├── SignUpFormDataSpec.scala
                            ├── UpdateInfoFormDataSpec.scala
                            └── UpdatePasswordFormDataSpec.scala
Download .txt
SYMBOL INDEX (11 symbols across 2 files)

FILE: server/src/main/resources/evolutions/default/1.sql
  type users (line 6) | CREATE TABLE users(
  type users_email_index (line 18) | CREATE INDEX users_email_index ON users USING BTREE (email)
  type user_logs (line 21) | CREATE TABLE user_logs (
  type user_logs_user_id_index (line 30) | CREATE INDEX user_logs_user_id_index ON user_logs USING BTREE (user_id)
  type user_tokens (line 32) | CREATE TABLE user_tokens (
  type user_tokens_user_id_index (line 43) | CREATE INDEX user_tokens_user_id_index ON user_tokens USING BTREE (user_id)
  type user_notifications (line 46) | CREATE TABLE user_notifications (
  type user_notifications_user_id_index (line 62) | CREATE INDEX user_notifications_user_id_index ON user_notifications USIN...
  type user_notifications_execute_at_index (line 63) | CREATE INDEX user_notifications_execute_at_index ON user_notifications U...

FILE: server/src/main/resources/evolutions/default/2.sql
  type background_jobs (line 5) | CREATE TABLE background_jobs (
  type background_jobs_execute_at_index (line 18) | CREATE INDEX background_jobs_execute_at_index ON background_jobs USING B...
Condensed preview — 247 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (449K chars).
[
  {
    "path": ".github/workflows/codepreview.yml",
    "chars": 2133,
    "preview": "name: Create preview environment\n\non:\n  pull_request:\n    branches: [ master ]\n  push:\n    branches: [ master, scala3 ]\n"
  },
  {
    "path": ".github/workflows/pull_request.yml",
    "chars": 1340,
    "preview": "name: Build the app\n\non:\n  push:\n    branches: [ master ]\n  pull_request:\n    branches: [ master ]\n\nconcurrency:\n  # Onl"
  },
  {
    "path": ".gitignore",
    "chars": 213,
    "preview": "target/\n.idea/\n.bsp/\n.vscode/\nlogs/\nadmin/build/\nweb/build/\nlocal.env\n\n# https://scalameta.org/metals/docs/editors/vscod"
  },
  {
    "path": ".nvmrc",
    "chars": 8,
    "preview": "16.7.0\n\n"
  },
  {
    "path": ".sbtopts",
    "chars": 67,
    "preview": "-J-Xmx4G\n-J-XX:MaxMetaspaceSize=4G\n-J-XX:+CMSClassUnloadingEnabled\n"
  },
  {
    "path": ".scalafmt.conf",
    "chars": 460,
    "preview": "version = 3.7.3\nproject.git = true\nproject.excludeFilters = [\n]\n\nrunner.dialect=scala3\n\nmaxColumn = 120\nassumeStandardLi"
  },
  {
    "path": ".sdkmanrc",
    "chars": 17,
    "preview": "java=11.0.16-tem\n"
  },
  {
    "path": "LICENSE",
    "chars": 1067,
    "preview": "MIT License\n\nCopyright (c) 2021 wiringbits\n\nPermission is hereby granted, free of charge, to any person obtaining a copy"
  },
  {
    "path": "README.md",
    "chars": 4505,
    "preview": "# Wiringbits Web Application Template\n\n![wiringbits](https://github.com/wiringbits/scala-webapp-template/workflows/Build"
  },
  {
    "path": "build.sbt",
    "chars": 15571,
    "preview": "import java.nio.file.Files\nimport java.nio.file.StandardCopyOption.REPLACE_EXISTING\n\nThisBuild / scalaVersion := \"3.3.0\""
  },
  {
    "path": "custom.webpack.config.js",
    "chars": 1316,
    "preview": "var path = require(\"path\");\nvar merge = require('webpack-merge');\nvar generated = require('./scalajs.webpack.config');\n\n"
  },
  {
    "path": "docs/README.md",
    "chars": 925,
    "preview": "# Wiringbits Scala WebApp template - Docs\n\n- [Setup development environment](./setup-dev-environment.md)\n- [Architecture"
  },
  {
    "path": "docs/architecture.md",
    "chars": 5881,
    "preview": "# Architecture\n\nThe following diagrams illustrate the overall project architecture.\n\n**Disclaimer** There are some parts"
  },
  {
    "path": "docs/design-decisions.md",
    "chars": 3328,
    "preview": "# Design decisions\nThis document explains why we took certain design decisions on how the project is built/structured.\n\n"
  },
  {
    "path": "docs/diagram-sources/architecture-infra.puml",
    "chars": 602,
    "preview": "@startuml\nTitle Wiringbits Scala WebApp Template - Infrastructure Diagram\n\nskinparam {\n    ArrowColor Red\n    linetype o"
  },
  {
    "path": "docs/diagram-sources/architecture-modules.puml",
    "chars": 717,
    "preview": "@startuml\nTitle Wiringbits Scala WebApp Template - Module Diagram\n\nskinparam {\n    ArrowColor Red\n}\n\npackage LibCommon {"
  },
  {
    "path": "docs/diagram-sources/architecture-server-actions.puml",
    "chars": 627,
    "preview": "@startuml\nTitle Wiringbits Scala WebApp Template - Server Architecture - Actions\n\nskinparam {\n    linetype ortho\n}\n\nskin"
  },
  {
    "path": "docs/diagram-sources/architecture-server-controllers.puml",
    "chars": 631,
    "preview": "@startuml\nTitle Wiringbits Scala WebApp Template - Server Architecture - Controllers\n\nskinparam {\n    linetype ortho\n}\n\n"
  },
  {
    "path": "docs/diagram-sources/architecture-server-daos.puml",
    "chars": 624,
    "preview": "@startuml\nTitle Wiringbits Scala WebApp Template - Server Architecture - DAOs\n\nskinparam {\n    linetype ortho\n}\n\nskinpar"
  },
  {
    "path": "docs/diagram-sources/architecture-server-external-apis.puml",
    "chars": 633,
    "preview": "@startuml\nTitle Wiringbits Scala WebApp Template - Server Architecture - External APIs\n\nskinparam {\n    linetype ortho\n}"
  },
  {
    "path": "docs/diagram-sources/architecture-server-repositories.puml",
    "chars": 632,
    "preview": "@startuml\nTitle Wiringbits Scala WebApp Template - Server Architecture - Repositories\n\nskinparam {\n    linetype ortho\n}\n"
  },
  {
    "path": "docs/diagram-sources/architecture-server-services.puml",
    "chars": 628,
    "preview": "@startuml\nTitle Wiringbits Scala WebApp Template - Server Architecture - Services\n\nskinparam {\n    linetype ortho\n}\n\nski"
  },
  {
    "path": "docs/diagram-sources/architecture-server.puml",
    "chars": 515,
    "preview": "@startuml\nTitle Wiringbits Scala WebApp Template - Server Architecture\n\nskinparam {\n    linetype ortho\n}\n\nrectangle Cont"
  },
  {
    "path": "docs/learning-material.md",
    "chars": 1706,
    "preview": "# Learning material\n\nThese are the tools used by the template, you don't need to master them all but being familiar with"
  },
  {
    "path": "docs/setup-dev-environment.md",
    "chars": 7211,
    "preview": "# Setup development environment\n\nLet's get started setting up your development environment.\n\n**NOTE** The instructions w"
  },
  {
    "path": "docs/swagger-integration.md",
    "chars": 2737,
    "preview": "# Swagger integration\n\nWe have a swagger integration so that users can explore the server API through swagger-ui.\n\nSome "
  },
  {
    "path": "infra/.gitignore",
    "chars": 69,
    "preview": "apps/\n.vault\nprod-hosts.ini\nconfig/server/demo.env.j2\n.scala-build/\n\n"
  },
  {
    "path": "infra/README.md",
    "chars": 2465,
    "preview": "# Infra\nThis project includes the necessary scripts and configuration files to deploy the applications to cloud servers "
  },
  {
    "path": "infra/admin.yml",
    "chars": 1182,
    "preview": "---\n- hosts: frontend\n  gather_facts: no\n  vars:\n    - webapp_source_zip: \"apps/admin.zip\"\n    - webapp_remote_file: \"ad"
  },
  {
    "path": "infra/config/nginx/admin-app-htpasswd",
    "chars": 42,
    "preview": "demo:$apr1$8bJ.PWGf$3IxfPeFYxWRQkCw3yvRfp0"
  },
  {
    "path": "infra/config/nginx/admin_app_site.j2",
    "chars": 1115,
    "preview": "server {\n  listen 80;\n\n  auth_basic \"Administrators only\";\n  auth_basic_user_file {{ nginx_admin_settings_file }};\n\n  se"
  },
  {
    "path": "infra/config/nginx/mime.types",
    "chars": 3957,
    "preview": "\ntypes {\n    text/html                             html htm shtml;\n    text/css                              css;\n    te"
  },
  {
    "path": "infra/config/nginx/nginx.conf",
    "chars": 1098,
    "preview": "user www-data;\nworker_processes auto;\npid /run/nginx.pid;\n\nevents {\n\tworker_connections 4096;\n\t# multi_accept on;\n}\n\nhtt"
  },
  {
    "path": "infra/config/nginx/preview_admin_app_site.j2",
    "chars": 1763,
    "preview": "server {\n\n    # listen [::]:443 ssl ipv6only=on; # managed by Certbot\n    listen 443 ssl; # managed by Certbot\n    ssl_c"
  },
  {
    "path": "infra/config/nginx/preview_web_app_site.j2",
    "chars": 2503,
    "preview": "server {\n\n    # listen [::]:443 ssl ipv6only=on; # managed by Certbot\n    listen 443 ssl; # managed by Certbot\n    ssl_c"
  },
  {
    "path": "infra/config/nginx/web_app_site.j2",
    "chars": 1859,
    "preview": "server {\n  listen 80;\n\n  server_name {{ web_app_domain }};\n  root {{ webapp_assets_directory }};\n  index index.html;\n\n  "
  },
  {
    "path": "infra/config/server/dev.env.j2",
    "chars": 580,
    "preview": "POSTGRES_DATABASE=\"server_db\"\nPOSTGRES_USERNAME=\"db_user\"\nPOSTGRES_PASSWORD=\"REPLACE_ME\"\nPOSTGRES_HOST=\"127.0.0.1\"\n\nPLAY"
  },
  {
    "path": "infra/config/server/server.service.j2",
    "chars": 442,
    "preview": "[Unit]\nDescription={{ app_systemd_service_name }}\n\n[Service]\nType=simple\nWorkingDirectory={{ app_home }}/{{ app_director"
  },
  {
    "path": "infra/demo-hosts.ini",
    "chars": 2799,
    "preview": "[webapp]\n[webapp:vars]\nansible_user=ubuntu\nansible_ssh_extra_args='-o StrictHostKeyChecking=no'\n\nserver_app_port=9000\n\n#"
  },
  {
    "path": "infra/nginx.yml",
    "chars": 935,
    "preview": "---\n- hosts: frontend\n  gather_facts: no\n  vars:\n    - nginx_config_file: \"config/nginx/nginx.conf\"\n    - nginx_mime_typ"
  },
  {
    "path": "infra/nginx_site_admin.yml",
    "chars": 1795,
    "preview": "---\n- hosts: frontend\n  gather_facts: no\n  vars:\n    - nginx_site_file: \"config/nginx/admin_app_site.j2\"\n    - nginx_adm"
  },
  {
    "path": "infra/nginx_site_web.yml",
    "chars": 1327,
    "preview": "---\n- hosts: frontend\n  gather_facts: no\n  vars:\n    - nginx_site_file: \"config/nginx/web_app_site.j2\"\n    - nginx_admin"
  },
  {
    "path": "infra/preview_nginx_site_admin.yml",
    "chars": 1419,
    "preview": "---\n- hosts: frontend\n  gather_facts: no\n  vars:\n    - nginx_site_file: \"config/nginx/preview_admin_app_site.j2\"\n    - n"
  },
  {
    "path": "infra/preview_nginx_site_web.yml",
    "chars": 953,
    "preview": "---\n- hosts: frontend\n  gather_facts: no\n  vars:\n    - nginx_site_file: \"config/nginx/preview_web_app_site.j2\"\n    - ngi"
  },
  {
    "path": "infra/scripts/build-admin.sh",
    "chars": 228,
    "preview": "#!/bin/bash\nset -e\nAPI_URL=$1\necho \"API_URL=$API_URL\" \\\n  && cd ../ \\\n  && API_URL=$API_URL sbt admin/build \\\n  && cd -\n"
  },
  {
    "path": "infra/scripts/build-server.sh",
    "chars": 216,
    "preview": "#!/bin/bash\nset -e\nAPP_SOURCE_ZIP=$1\necho \"APP_SOURCE_ZIP=$APP_SOURCE_ZIP\"\ncd ../ && SWAGGER_API_BASEPATH=\"/api\" sbt ser"
  },
  {
    "path": "infra/scripts/build-web.sh",
    "chars": 216,
    "preview": "#!/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"
  },
  {
    "path": "infra/server.yml",
    "chars": 2979,
    "preview": "---\n- hosts: backend\n  gather_facts: no\n  vars:\n    - app_source_zip: \"apps/server.zip\"\n    - app_source_zip_remote: \"se"
  },
  {
    "path": "infra/setup-postgres.md",
    "chars": 1371,
    "preview": "# Setup postgres\nThis is the manual way to set up postgres for a test server, for production it is recommended to use a "
  },
  {
    "path": "infra/test-hosts.ini",
    "chars": 2798,
    "preview": "[webapp]\n[webapp:vars]\nansible_user=ubuntu\nansible_ssh_extra_args='-o StrictHostKeyChecking=no'\n\nserver_app_port=9000\n\n#"
  },
  {
    "path": "infra/web.yml",
    "chars": 1172,
    "preview": "---\n- hosts: frontend\n  gather_facts: no\n  vars:\n    - webapp_source_zip: \"apps/web.zip\"\n    - webapp_remote_file: \"web."
  },
  {
    "path": "lib/api/shared/src/main/scala/net/wiringbits/api/ApiClient.scala",
    "chars": 5492,
    "preview": "package net.wiringbits.api\n\nimport net.wiringbits.api.endpoints.*\nimport net.wiringbits.api.models.*\nimport net.wiringbi"
  },
  {
    "path": "lib/api/shared/src/main/scala/net/wiringbits/api/endpoints/AdminEndpoints.scala",
    "chars": 2114,
    "preview": "package net.wiringbits.api.endpoints\n\nimport net.wiringbits.api.models\nimport net.wiringbits.api.models.admin.{AdminGetU"
  },
  {
    "path": "lib/api/shared/src/main/scala/net/wiringbits/api/endpoints/AuthEndpoints.scala",
    "chars": 2867,
    "preview": "package net.wiringbits.api.endpoints\n\nimport net.wiringbits.api.models\nimport net.wiringbits.api.models.auth.{GetCurrent"
  },
  {
    "path": "lib/api/shared/src/main/scala/net/wiringbits/api/endpoints/EnvironmentConfigEndpoints.scala",
    "chars": 937,
    "preview": "package net.wiringbits.api.endpoints\n\nimport net.wiringbits.api.models\nimport net.wiringbits.api.models.ErrorResponse\nim"
  },
  {
    "path": "lib/api/shared/src/main/scala/net/wiringbits/api/endpoints/HealthEndpoints.scala",
    "chars": 390,
    "preview": "package net.wiringbits.api.endpoints\n\nimport sttp.tapir.*\n\nobject HealthEndpoints {\n  private val baseEndpoint = endpoin"
  },
  {
    "path": "lib/api/shared/src/main/scala/net/wiringbits/api/endpoints/UsersEndpoints.scala",
    "chars": 7160,
    "preview": "package net.wiringbits.api.endpoints\n\nimport net.wiringbits.api.models.*\nimport net.wiringbits.api.models.users.*\nimport"
  },
  {
    "path": "lib/api/shared/src/main/scala/net/wiringbits/api/endpoints/package.scala",
    "chars": 1598,
    "preview": "package net.wiringbits.api\n\nimport net.wiringbits.api.models.{ErrorResponse, errorResponseFormat}\nimport sttp.model.Stat"
  },
  {
    "path": "lib/api/shared/src/main/scala/net/wiringbits/api/models/PlayErrorResponse.scala",
    "chars": 850,
    "preview": "package net.wiringbits.api.models\n\nimport play.api.libs.json.{Format, Json}\nimport sttp.tapir.Schema\n\n// play json error"
  },
  {
    "path": "lib/api/shared/src/main/scala/net/wiringbits/api/models/admin/AdminGetUserLogs.scala",
    "chars": 959,
    "preview": "package net.wiringbits.api.models.admin\n\nimport net.wiringbits.api.models.*\nimport play.api.libs.json.{Format, Json}\nimp"
  },
  {
    "path": "lib/api/shared/src/main/scala/net/wiringbits/api/models/admin/AdminGetUsers.scala",
    "chars": 951,
    "preview": "package net.wiringbits.api.models.admin\n\nimport net.wiringbits.api.models.*\nimport net.wiringbits.common.models.{Email, "
  },
  {
    "path": "lib/api/shared/src/main/scala/net/wiringbits/api/models/auth/GetCurrentUser.scala",
    "chars": 630,
    "preview": "package net.wiringbits.api.models.auth\n\nimport net.wiringbits.api.models.*\nimport net.wiringbits.common.models.{Email, N"
  },
  {
    "path": "lib/api/shared/src/main/scala/net/wiringbits/api/models/auth/Login.scala",
    "chars": 922,
    "preview": "package net.wiringbits.api.models.auth\n\nimport net.wiringbits.api.models.*\nimport net.wiringbits.common.models.{Captcha,"
  },
  {
    "path": "lib/api/shared/src/main/scala/net/wiringbits/api/models/auth/Logout.scala",
    "chars": 727,
    "preview": "package net.wiringbits.api.models.auth\n\nimport play.api.libs.json.{Format, Json}\nimport sttp.tapir.Schema\n\nobject Logout"
  },
  {
    "path": "lib/api/shared/src/main/scala/net/wiringbits/api/models/environmentconfig/GetEnvironmentConfig.scala",
    "chars": 482,
    "preview": "package net.wiringbits.api.models.environmentconfig\n\nimport play.api.libs.json.{Format, Json}\nimport sttp.tapir.Schema\n\n"
  },
  {
    "path": "lib/api/shared/src/main/scala/net/wiringbits/api/models/package.scala",
    "chars": 1201,
    "preview": "package net.wiringbits.api\n\nimport net.wiringbits.webapp.common.models.WrappedString\nimport play.api.libs.json.*\nimport "
  },
  {
    "path": "lib/api/shared/src/main/scala/net/wiringbits/api/models/users/CreateUser.scala",
    "chars": 936,
    "preview": "package net.wiringbits.api.models.users\n\nimport net.wiringbits.api.models.*\nimport net.wiringbits.common.models.{Captcha"
  },
  {
    "path": "lib/api/shared/src/main/scala/net/wiringbits/api/models/users/ForgotPassword.scala",
    "chars": 890,
    "preview": "package net.wiringbits.api.models.users\n\nimport net.wiringbits.api.models.*\nimport net.wiringbits.common.models.{Captcha"
  },
  {
    "path": "lib/api/shared/src/main/scala/net/wiringbits/api/models/users/GetUserLogs.scala",
    "chars": 905,
    "preview": "package net.wiringbits.api.models.users\n\nimport net.wiringbits.api.models.*\nimport play.api.libs.json.{Format, Json}\nimp"
  },
  {
    "path": "lib/api/shared/src/main/scala/net/wiringbits/api/models/users/ResetPassword.scala",
    "chars": 944,
    "preview": "package net.wiringbits.api.models.users\n\nimport net.wiringbits.api.models.*\nimport net.wiringbits.common.models.{Email, "
  },
  {
    "path": "lib/api/shared/src/main/scala/net/wiringbits/api/models/users/SendEmailVerificationToken.scala",
    "chars": 1024,
    "preview": "package net.wiringbits.api.models.users\n\nimport net.wiringbits.api.models.*\nimport net.wiringbits.common.models.{Captcha"
  },
  {
    "path": "lib/api/shared/src/main/scala/net/wiringbits/api/models/users/UpdatePassword.scala",
    "chars": 903,
    "preview": "package net.wiringbits.api.models.users\n\nimport net.wiringbits.api.models.*\nimport net.wiringbits.common.models.Password"
  },
  {
    "path": "lib/api/shared/src/main/scala/net/wiringbits/api/models/users/UpdateUser.scala",
    "chars": 827,
    "preview": "package net.wiringbits.api.models.users\n\nimport net.wiringbits.api.models.*\nimport net.wiringbits.common.models.Name\nimp"
  },
  {
    "path": "lib/api/shared/src/main/scala/net/wiringbits/api/models/users/VerifyEmail.scala",
    "chars": 832,
    "preview": "package net.wiringbits.api.models.users\n\nimport net.wiringbits.common.models.UserToken\nimport play.api.libs.json.{Format"
  },
  {
    "path": "lib/api/shared/src/main/scala/net/wiringbits/api/utils/Formatter.scala",
    "chars": 515,
    "preview": "package net.wiringbits.api.utils\n\nimport java.time.Instant\n\nobject Formatter {\n\n  def instant(item: Instant): String = {"
  },
  {
    "path": "lib/common/js/src/test/scala/java/security/SecureRandom.scala",
    "chars": 3779,
    "preview": "package java.security\n\nimport scala.scalajs.js\nimport scala.scalajs.js.typedarray.*\n\n// DISCLAIMER: This is almost ident"
  },
  {
    "path": "lib/common/shared/src/main/scala/net/wiringbits/common/ErrorMessages.scala",
    "chars": 161,
    "preview": "package net.wiringbits.common\n\nobject ErrorMessages {\n  val emailNotVerified = \"The email is not verified, check your sp"
  },
  {
    "path": "lib/common/shared/src/main/scala/net/wiringbits/common/models/Captcha.scala",
    "chars": 632,
    "preview": "package net.wiringbits.common.models\n\nimport net.wiringbits.webapp.common.models.WrappedString\nimport net.wiringbits.web"
  },
  {
    "path": "lib/common/shared/src/main/scala/net/wiringbits/common/models/Email.scala",
    "chars": 739,
    "preview": "package net.wiringbits.common.models\n\nimport net.wiringbits.webapp.common.models.WrappedString\nimport net.wiringbits.web"
  },
  {
    "path": "lib/common/shared/src/main/scala/net/wiringbits/common/models/Name.scala",
    "chars": 706,
    "preview": "package net.wiringbits.common.models\n\nimport net.wiringbits.webapp.common.models.WrappedString\nimport net.wiringbits.web"
  },
  {
    "path": "lib/common/shared/src/main/scala/net/wiringbits/common/models/Password.scala",
    "chars": 708,
    "preview": "package net.wiringbits.common.models\n\nimport net.wiringbits.webapp.common.models.WrappedString\nimport net.wiringbits.web"
  },
  {
    "path": "lib/common/shared/src/main/scala/net/wiringbits/common/models/UserToken.scala",
    "chars": 591,
    "preview": "package net.wiringbits.common.models\n\nimport play.api.libs.json.{Format, Json}\n\nimport java.util.UUID\nimport scala.util."
  },
  {
    "path": "lib/common/shared/src/test/scala/net/wiringbits/common/models/EmailSpec.scala",
    "chars": 917,
    "preview": "package net.wiringbits.common.models\n\nimport org.scalatest.matchers.must.Matchers.*\nimport org.scalatest.wordspec.AnyWor"
  },
  {
    "path": "lib/common/shared/src/test/scala/net/wiringbits/common/models/NameSpec.scala",
    "chars": 588,
    "preview": "package net.wiringbits.common.models\n\nimport org.scalatest.matchers.must.Matchers.*\nimport org.scalatest.wordspec.AnyWor"
  },
  {
    "path": "lib/common/shared/src/test/scala/net/wiringbits/common/models/PasswordSpec.scala",
    "chars": 635,
    "preview": "package net.wiringbits.common.models\n\nimport org.scalatest.matchers.must.Matchers.*\nimport org.scalatest.wordspec.AnyWor"
  },
  {
    "path": "lib/common/shared/src/test/scala/net/wiringbits/common/models/UserTokenSpec.scala",
    "chars": 1327,
    "preview": "package net.wiringbits.common.models\n\nimport org.scalatest.matchers.must.Matchers.{be, must}\nimport org.scalatest.wordsp"
  },
  {
    "path": "lib/ui/src/main/scala/net/wiringbits/ui/components/core/widgets/ValidatedTextInput.scala",
    "chars": 1479,
    "preview": "package net.wiringbits.ui.components.core.widgets\n\nimport com.olvind.mui.muiMaterial.components as mui\n\nimport net.wirin"
  },
  {
    "path": "lib/ui/src/main/scala/net/wiringbits/ui/components/inputs/inputs.scala",
    "chars": 359,
    "preview": "package net.wiringbits.ui.components\n\nimport net.wiringbits.common.models.{Email, Name, Password}\nimport net.wiringbits."
  },
  {
    "path": "project/build.properties",
    "chars": 20,
    "preview": "sbt.version = 1.7.3\n"
  },
  {
    "path": "project/plugins.sbt",
    "chars": 638,
    "preview": "// while there are some eviction errors, plugins seem to be compatible so far\nevictionErrorLevel := sbt.util.Level.Warn\n"
  },
  {
    "path": "server/src/main/resources/application.conf",
    "chars": 4289,
    "preview": "# https://www.playframework.com/documentation/latest/Configuration\n\n# Swagger - be aware these are used at compile time\n"
  },
  {
    "path": "server/src/main/resources/evolutions/default/1.sql",
    "chars": 2263,
    "preview": "\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 NO"
  },
  {
    "path": "server/src/main/resources/evolutions/default/2.sql",
    "chars": 714,
    "preview": "\n-- !Ups\n\n-- Stores the background jobs from the app\nCREATE TABLE background_jobs (\n    background_job_id UUID NOT NULL,"
  },
  {
    "path": "server/src/main/resources/logback.xml",
    "chars": 2772,
    "preview": "<!-- https://www.playframework.com/documentation/latest/SettingsLogger -->\n<configuration>\n\n    <conversionRule conversi"
  },
  {
    "path": "server/src/main/resources/messages",
    "chars": 63,
    "preview": "# https://www.playframework.com/documentation/latest/ScalaI18N\n"
  },
  {
    "path": "server/src/main/resources/routes",
    "chars": 289,
    "preview": "# Routes\n# This file defines all application routes (Higher priority routes first)\n# https://www.playframework.com/docum"
  },
  {
    "path": "server/src/main/scala/PekkoStream.scala",
    "chars": 6120,
    "preview": "/*\n * Copyright (C) from 2022 The Play Framework Contributors <https://github.com/playframework>, 2011-2021 Lightbend In"
  },
  {
    "path": "server/src/main/scala/controllers/AdminController.scala",
    "chars": 1696,
    "preview": "package controllers\n\nimport net.wiringbits.api.endpoints.AdminEndpoints\nimport net.wiringbits.api.models.ErrorResponse\ni"
  },
  {
    "path": "server/src/main/scala/controllers/ApiRouter.scala",
    "chars": 1854,
    "preview": "package controllers\n\nimport net.wiringbits.api.endpoints.*\nimport net.wiringbits.config.SwaggerConfig\nimport org.apache."
  },
  {
    "path": "server/src/main/scala/controllers/AuthController.scala",
    "chars": 1883,
    "preview": "package controllers\n\nimport net.wiringbits.actions.auth.{GetUserAction, LoginAction}\nimport net.wiringbits.api.endpoints"
  },
  {
    "path": "server/src/main/scala/controllers/EnvironmentConfigController.scala",
    "chars": 1136,
    "preview": "package controllers\n\nimport net.wiringbits.actions.environmentconfig.GetEnvironmentConfigAction\nimport net.wiringbits.ap"
  },
  {
    "path": "server/src/main/scala/controllers/HealthController.scala",
    "chars": 697,
    "preview": "package controllers\n\nimport net.wiringbits.api.endpoints.HealthEndpoints\nimport sttp.capabilities.WebSockets\nimport sttp"
  },
  {
    "path": "server/src/main/scala/controllers/UsersController.scala",
    "chars": 4180,
    "preview": "package controllers\n\nimport net.wiringbits.actions.*\nimport net.wiringbits.actions.users.*\nimport net.wiringbits.api.end"
  },
  {
    "path": "server/src/main/scala/controllers/package.scala",
    "chars": 2505,
    "preview": "import net.wiringbits.api.models.{ErrorResponse, errorResponseFormat}\nimport org.slf4j.LoggerFactory\nimport play.api.mvc"
  },
  {
    "path": "server/src/main/scala/net/wiringbits/actions/auth/GetUserAction.scala",
    "chars": 897,
    "preview": "package net.wiringbits.actions.auth\n\nimport io.scalaland.chimney.dsl.transformInto\nimport net.wiringbits.api.models.auth"
  },
  {
    "path": "server/src/main/scala/net/wiringbits/actions/auth/LoginAction.scala",
    "chars": 1174,
    "preview": "package net.wiringbits.actions.auth\n\nimport io.scalaland.chimney.dsl.transformInto\nimport net.wiringbits.api.models.auth"
  },
  {
    "path": "server/src/main/scala/net/wiringbits/actions/environmentconfig/GetEnvironmentConfigAction.scala",
    "chars": 462,
    "preview": "package net.wiringbits.actions.environmentconfig\n\nimport net.wiringbits.api.models.environmentconfig.GetEnvironmentConfi"
  },
  {
    "path": "server/src/main/scala/net/wiringbits/actions/internal/StreamPendingBackgroundJobsForeverAction.scala",
    "chars": 1304,
    "preview": "package net.wiringbits.actions.internal\n\nimport org.apache.pekko.actor.ActorSystem\nimport org.apache.pekko.stream.scalad"
  },
  {
    "path": "server/src/main/scala/net/wiringbits/actions/users/CreateUserAction.scala",
    "chars": 2212,
    "preview": "package net.wiringbits.actions.users\n\nimport io.scalaland.chimney.dsl.transformInto\nimport net.wiringbits.api.models.use"
  },
  {
    "path": "server/src/main/scala/net/wiringbits/actions/users/ForgotPasswordAction.scala",
    "chars": 1164,
    "preview": "package net.wiringbits.actions.users\n\nimport net.wiringbits.api.models.users.ForgotPassword\nimport net.wiringbits.apis.R"
  },
  {
    "path": "server/src/main/scala/net/wiringbits/actions/users/GetUserLogsAction.scala",
    "chars": 638,
    "preview": "package net.wiringbits.actions.users\n\nimport io.scalaland.chimney.dsl.transformInto\nimport net.wiringbits.api.models.use"
  },
  {
    "path": "server/src/main/scala/net/wiringbits/actions/users/ResetPasswordAction.scala",
    "chars": 1665,
    "preview": "package net.wiringbits.actions.users\n\nimport net.wiringbits.api.models.users.ResetPassword\nimport net.wiringbits.common."
  },
  {
    "path": "server/src/main/scala/net/wiringbits/actions/users/SendEmailVerificationTokenAction.scala",
    "chars": 1340,
    "preview": "package net.wiringbits.actions.users\n\nimport net.wiringbits.api.models.users.SendEmailVerificationToken\nimport net.wirin"
  },
  {
    "path": "server/src/main/scala/net/wiringbits/actions/users/UpdatePasswordAction.scala",
    "chars": 929,
    "preview": "package net.wiringbits.actions.users\n\nimport net.wiringbits.api.models.users.UpdatePassword\nimport net.wiringbits.reposi"
  },
  {
    "path": "server/src/main/scala/net/wiringbits/actions/users/UpdateUserAction.scala",
    "chars": 663,
    "preview": "package net.wiringbits.actions.users\n\nimport net.wiringbits.api.models.users.UpdateUser\nimport net.wiringbits.repositori"
  },
  {
    "path": "server/src/main/scala/net/wiringbits/actions/users/VerifyUserEmailAction.scala",
    "chars": 1524,
    "preview": "package net.wiringbits.actions.users\n\nimport net.wiringbits.api.models.users.VerifyEmail\nimport net.wiringbits.config.Us"
  },
  {
    "path": "server/src/main/scala/net/wiringbits/apis/EmailApi.scala",
    "chars": 663,
    "preview": "package net.wiringbits.apis\n\nimport net.wiringbits.apis.models.EmailRequest\nimport org.slf4j.LoggerFactory\n\nimport javax"
  },
  {
    "path": "server/src/main/scala/net/wiringbits/apis/EmailApiAWSImpl.scala",
    "chars": 2437,
    "preview": "package net.wiringbits.apis\n\nimport net.wiringbits.apis.models.EmailRequest\nimport net.wiringbits.config.{AWSConfig, Ema"
  },
  {
    "path": "server/src/main/scala/net/wiringbits/apis/ReCaptchaApi.scala",
    "chars": 803,
    "preview": "package net.wiringbits.apis\n\nimport net.wiringbits.common.models.Captcha\nimport net.wiringbits.config.ReCaptchaConfig\nim"
  },
  {
    "path": "server/src/main/scala/net/wiringbits/apis/models/EmailRequest.scala",
    "chars": 186,
    "preview": "package net.wiringbits.apis.models\n\nimport net.wiringbits.common.models.Email\nimport net.wiringbits.util.EmailMessage\n\nc"
  },
  {
    "path": "server/src/main/scala/net/wiringbits/config/AWSConfig.scala",
    "chars": 784,
    "preview": "package net.wiringbits.config\n\nimport net.wiringbits.models.{AWSAccessKeyId, AWSSecretAccessKey}\nimport play.api.Configu"
  },
  {
    "path": "server/src/main/scala/net/wiringbits/config/BackgroundJobsExecutorConfig.scala",
    "chars": 494,
    "preview": "package net.wiringbits.config\n\nimport play.api.Configuration\n\nimport scala.concurrent.duration.FiniteDuration\n\ncase clas"
  },
  {
    "path": "server/src/main/scala/net/wiringbits/config/EmailConfig.scala",
    "chars": 505,
    "preview": "package net.wiringbits.config\n\nimport play.api.Configuration\n\ncase class EmailConfig(senderAddress: String, provider: St"
  },
  {
    "path": "server/src/main/scala/net/wiringbits/config/ReCaptchaConfig.scala",
    "chars": 582,
    "preview": "package net.wiringbits.config\n\nimport net.wiringbits.models.{ReCaptchaSecret, ReCaptchaSiteKey}\nimport play.api.Configur"
  },
  {
    "path": "server/src/main/scala/net/wiringbits/config/SwaggerConfig.scala",
    "chars": 933,
    "preview": "package net.wiringbits.config\n\nimport play.api.Configuration\n\ncase class SwaggerConfig(basePath: String, info: SwaggerCo"
  },
  {
    "path": "server/src/main/scala/net/wiringbits/config/UserTokensConfig.scala",
    "chars": 879,
    "preview": "package net.wiringbits.config\n\nimport play.api.Configuration\n\nimport scala.concurrent.duration.FiniteDuration\n\ncase clas"
  },
  {
    "path": "server/src/main/scala/net/wiringbits/config/WebAppConfig.scala",
    "chars": 324,
    "preview": "package net.wiringbits.config\n\nimport play.api.Configuration\n\ncase class WebAppConfig(host: String) {\n  override def toS"
  },
  {
    "path": "server/src/main/scala/net/wiringbits/executors/DatabaseExecutionContext.scala",
    "chars": 475,
    "preview": "package net.wiringbits.executors\n\nimport org.apache.pekko.actor.ActorSystem\nimport play.api.libs.concurrent.CustomExecut"
  },
  {
    "path": "server/src/main/scala/net/wiringbits/models/AWSAccessKeyId.scala",
    "chars": 345,
    "preview": "package net.wiringbits.models\n\nimport com.typesafe.config.Config\nimport play.api.ConfigLoader\n\ncase class AWSAccessKeyId"
  },
  {
    "path": "server/src/main/scala/net/wiringbits/models/AWSSecretAccessKey.scala",
    "chars": 361,
    "preview": "package net.wiringbits.models\n\nimport com.typesafe.config.Config\nimport play.api.ConfigLoader\n\ncase class AWSSecretAcces"
  },
  {
    "path": "server/src/main/scala/net/wiringbits/models/ReCaptchaSecret.scala",
    "chars": 349,
    "preview": "package net.wiringbits.models\n\nimport com.typesafe.config.Config\nimport play.api.ConfigLoader\n\ncase class ReCaptchaSecre"
  },
  {
    "path": "server/src/main/scala/net/wiringbits/models/ReCaptchaSiteKey.scala",
    "chars": 324,
    "preview": "package net.wiringbits.models\n\nimport com.typesafe.config.Config\nimport play.api.ConfigLoader\n\ncase class ReCaptchaSiteK"
  },
  {
    "path": "server/src/main/scala/net/wiringbits/models/SecretValue.scala",
    "chars": 191,
    "preview": "package net.wiringbits.models\n\nimport net.wiringbits.util.StringUtils.Implicits.StringUtilsExt\n\nabstract class SecretVal"
  },
  {
    "path": "server/src/main/scala/net/wiringbits/models/jobs/BackgroundJobPayload.scala",
    "chars": 747,
    "preview": "package net.wiringbits.models.jobs\n\nimport net.wiringbits.common.models.Email\nimport play.api.libs.json.{Format, Json, W"
  },
  {
    "path": "server/src/main/scala/net/wiringbits/models/jobs/BackgroundJobStatus.scala",
    "chars": 419,
    "preview": "package net.wiringbits.models.jobs\n\nimport enumeratum.EnumEntry.Uppercase\nimport enumeratum.{Enum, EnumEntry}\n\nsealed tr"
  },
  {
    "path": "server/src/main/scala/net/wiringbits/models/jobs/BackgroundJobType.scala",
    "chars": 478,
    "preview": "package net.wiringbits.models.jobs\n\nimport enumeratum.EnumEntry.Uppercase\nimport enumeratum.{Enum, EnumEntry}\n\nsealed tr"
  },
  {
    "path": "server/src/main/scala/net/wiringbits/modules/ApisModule.scala",
    "chars": 961,
    "preview": "package net.wiringbits.modules\n\nimport com.google.inject.{AbstractModule, Provider}\nimport net.wiringbits.apis.{EmailApi"
  },
  {
    "path": "server/src/main/scala/net/wiringbits/modules/ClockModule.scala",
    "chars": 239,
    "preview": "package net.wiringbits.modules\n\nimport com.google.inject.AbstractModule\n\nimport java.time.Clock\n\nclass ClockModule exten"
  },
  {
    "path": "server/src/main/scala/net/wiringbits/modules/ConfigModule.scala",
    "chars": 1878,
    "preview": "package net.wiringbits.modules\n\nimport com.google.inject.{AbstractModule, Provides}\nimport net.wiringbits.config.*\nimpor"
  },
  {
    "path": "server/src/main/scala/net/wiringbits/modules/ExecutorsModule.scala",
    "chars": 340,
    "preview": "package net.wiringbits.modules\n\nimport com.google.inject.AbstractModule\nimport net.wiringbits.executors.DatabaseExecutio"
  },
  {
    "path": "server/src/main/scala/net/wiringbits/modules/TasksModule.scala",
    "chars": 280,
    "preview": "package net.wiringbits.modules\n\nimport com.google.inject.AbstractModule\nimport net.wiringbits.tasks.BackgroundJobsExecut"
  },
  {
    "path": "server/src/main/scala/net/wiringbits/repositories/BackgroundJobsRepository.scala",
    "chars": 1659,
    "preview": "package net.wiringbits.repositories\n\nimport net.wiringbits.executors.DatabaseExecutionContext\nimport net.wiringbits.repo"
  },
  {
    "path": "server/src/main/scala/net/wiringbits/repositories/UserLogsRepository.scala",
    "chars": 954,
    "preview": "package net.wiringbits.repositories\n\nimport net.wiringbits.executors.DatabaseExecutionContext\nimport net.wiringbits.repo"
  },
  {
    "path": "server/src/main/scala/net/wiringbits/repositories/UserTokensRepository.scala",
    "chars": 1084,
    "preview": "package net.wiringbits.repositories\n\nimport net.wiringbits.executors.DatabaseExecutionContext\nimport net.wiringbits.repo"
  },
  {
    "path": "server/src/main/scala/net/wiringbits/repositories/UsersRepository.scala",
    "chars": 4230,
    "preview": "package net.wiringbits.repositories\n\nimport net.wiringbits.common.models.{Email, Name}\nimport net.wiringbits.config.User"
  },
  {
    "path": "server/src/main/scala/net/wiringbits/repositories/daos/BackgroundJobDAO.scala",
    "chars": 2481,
    "preview": "package net.wiringbits.repositories.daos\n\nimport anorm.postgresql.*\nimport net.wiringbits.models.jobs.BackgroundJobStatu"
  },
  {
    "path": "server/src/main/scala/net/wiringbits/repositories/daos/UserLogsDAO.scala",
    "chars": 847,
    "preview": "package net.wiringbits.repositories.daos\n\nimport net.wiringbits.repositories.models.UserLog\n\nimport java.sql.Connection\n"
  },
  {
    "path": "server/src/main/scala/net/wiringbits/repositories/daos/UserTokensDAO.scala",
    "chars": 1664,
    "preview": "package net.wiringbits.repositories.daos\n\nimport anorm.SqlStringInterpolation\nimport net.wiringbits.repositories.models."
  },
  {
    "path": "server/src/main/scala/net/wiringbits/repositories/daos/UsersDAO.scala",
    "chars": 2289,
    "preview": "package net.wiringbits.repositories.daos\n\nimport net.wiringbits.common.models.{Email, Name}\nimport net.wiringbits.reposi"
  },
  {
    "path": "server/src/main/scala/net/wiringbits/repositories/daos/package.scala",
    "chars": 2924,
    "preview": "package net.wiringbits.repositories\n\nimport anorm.*\nimport anorm.SqlParser.*\nimport anorm.postgresql.*\nimport net.wiring"
  },
  {
    "path": "server/src/main/scala/net/wiringbits/repositories/models/BackgroundJobData.scala",
    "chars": 736,
    "preview": "package net.wiringbits.repositories.models\n\nimport net.wiringbits.models.jobs.{BackgroundJobPayload, BackgroundJobStatus"
  },
  {
    "path": "server/src/main/scala/net/wiringbits/repositories/models/User.scala",
    "chars": 421,
    "preview": "package net.wiringbits.repositories.models\n\nimport net.wiringbits.common.models.{Email, Name}\n\nimport java.time.Instant\n"
  },
  {
    "path": "server/src/main/scala/net/wiringbits/repositories/models/UserLog.scala",
    "chars": 274,
    "preview": "package net.wiringbits.repositories.models\n\nimport java.time.Instant\nimport java.util.UUID\n\ncase class UserLog(userLogId"
  },
  {
    "path": "server/src/main/scala/net/wiringbits/repositories/models/UserToken.scala",
    "chars": 432,
    "preview": "package net.wiringbits.repositories.models\n\nimport java.time.Instant\nimport java.util.UUID\n\ncase class UserToken(\n    id"
  },
  {
    "path": "server/src/main/scala/net/wiringbits/repositories/models/UserTokenType.scala",
    "chars": 364,
    "preview": "package net.wiringbits.repositories.models\n\nimport enumeratum.EnumEntry.Uppercase\nimport enumeratum.{Enum, EnumEntry}\n\ns"
  },
  {
    "path": "server/src/main/scala/net/wiringbits/services/AdminService.scala",
    "chars": 934,
    "preview": "package net.wiringbits.services\n\nimport io.scalaland.chimney.dsl.transformInto\nimport net.wiringbits.api.models.admin.{A"
  },
  {
    "path": "server/src/main/scala/net/wiringbits/tasks/BackgroundJobsExecutorTask.scala",
    "chars": 3388,
    "preview": "package net.wiringbits.tasks\n\nimport org.apache.pekko.actor.ActorSystem\nimport com.google.inject.Inject\nimport net.wirin"
  },
  {
    "path": "server/src/main/scala/net/wiringbits/util/DelayGenerator.scala",
    "chars": 226,
    "preview": "package net.wiringbits.util\n\nobject DelayGenerator {\n  def createDelay(\n      retry: Int,\n      factor: Int = 2\n  ): Lon"
  },
  {
    "path": "server/src/main/scala/net/wiringbits/util/EmailMessage.scala",
    "chars": 2380,
    "preview": "package net.wiringbits.util\n\nimport net.wiringbits.common.models.Name\nimport org.apache.commons.text.StringEscapeUtils\n\n"
  },
  {
    "path": "server/src/main/scala/net/wiringbits/util/EmailsHelper.scala",
    "chars": 2879,
    "preview": "package net.wiringbits.util\n\nimport net.wiringbits.apis.EmailApi\nimport net.wiringbits.apis.models.EmailRequest\nimport n"
  },
  {
    "path": "server/src/main/scala/net/wiringbits/util/StringUtils.scala",
    "chars": 487,
    "preview": "package net.wiringbits.util\n\nobject StringUtils {\n\n  def mask(value: String, prefixSize: Int, suffixSize: Int): String ="
  },
  {
    "path": "server/src/main/scala/net/wiringbits/util/TokenGenerator.scala",
    "chars": 156,
    "preview": "package net.wiringbits.util\n\nimport java.util.UUID\nimport javax.inject.Inject\n\nclass TokenGenerator @Inject() () {\n  def"
  },
  {
    "path": "server/src/main/scala/net/wiringbits/util/TokensHelper.scala",
    "chars": 648,
    "preview": "package net.wiringbits.util\n\nimport jakarta.xml.bind.DatatypeConverter\n\nobject TokensHelper {\n\n  def doHMACSHA1(value: A"
  },
  {
    "path": "server/src/main/scala/net/wiringbits/validations/ValidateCaptcha.scala",
    "chars": 472,
    "preview": "package net.wiringbits.validations\n\nimport net.wiringbits.apis.ReCaptchaApi\nimport net.wiringbits.common.models.Captcha\n"
  },
  {
    "path": "server/src/main/scala/net/wiringbits/validations/ValidateEmailIsAvailable.scala",
    "chars": 493,
    "preview": "package net.wiringbits.validations\n\nimport net.wiringbits.common.models.Email\nimport net.wiringbits.repositories.UsersRe"
  },
  {
    "path": "server/src/main/scala/net/wiringbits/validations/ValidateEmailIsRegistered.scala",
    "chars": 493,
    "preview": "package net.wiringbits.validations\n\nimport net.wiringbits.common.models.Email\nimport net.wiringbits.repositories.UsersRe"
  },
  {
    "path": "server/src/main/scala/net/wiringbits/validations/ValidatePasswordMatches.scala",
    "chars": 438,
    "preview": "package net.wiringbits.validations\n\nimport net.wiringbits.common.models.Password\nimport net.wiringbits.repositories.mode"
  },
  {
    "path": "server/src/main/scala/net/wiringbits/validations/ValidateUserIsNotVerified.scala",
    "chars": 274,
    "preview": "package net.wiringbits.validations\n\nimport net.wiringbits.repositories.models.User\n\nobject ValidateUserIsNotVerified {\n "
  },
  {
    "path": "server/src/main/scala/net/wiringbits/validations/ValidateUserToken.scala",
    "chars": 325,
    "preview": "package net.wiringbits.validations\n\nimport net.wiringbits.repositories.models.UserToken\n\nimport java.time.Clock\n\nobject "
  },
  {
    "path": "server/src/main/scala/net/wiringbits/validations/ValidateVerifiedUser.scala",
    "chars": 315,
    "preview": "package net.wiringbits.validations\n\nimport net.wiringbits.common.ErrorMessages\nimport net.wiringbits.repositories.models"
  },
  {
    "path": "server/src/test/scala/controllers/AdminControllerSpec.scala",
    "chars": 2396,
    "preview": "package controllers\n\nimport com.dimafeng.testcontainers.PostgreSQLContainer\nimport controllers.common.PlayPostgresSpec\ni"
  },
  {
    "path": "server/src/test/scala/controllers/AuthControllerSpec.scala",
    "chars": 6465,
    "preview": "package controllers\n\nimport com.dimafeng.testcontainers.PostgreSQLContainer\nimport controllers.common.PlayPostgresSpec\ni"
  },
  {
    "path": "server/src/test/scala/controllers/EnvironmentConfigControllerSpec.scala",
    "chars": 557,
    "preview": "package controllers\n\nimport controllers.common.PlayPostgresSpec\nimport net.wiringbits.config.ReCaptchaConfig\nimport org."
  },
  {
    "path": "server/src/test/scala/controllers/UsersControllerSpec.scala",
    "chars": 12495,
    "preview": "package controllers\n\nimport com.dimafeng.testcontainers.PostgreSQLContainer\nimport controllers.common.PlayPostgresSpec\ni"
  },
  {
    "path": "server/src/test/scala/controllers/common/PlayAPISpec.scala",
    "chars": 3513,
    "preview": "package controllers.common\n\nimport org.scalatest.concurrent.ScalaFutures\nimport org.scalatestplus.play.PlaySpec\nimport o"
  },
  {
    "path": "server/src/test/scala/controllers/common/PlayPostgresSpec.scala",
    "chars": 2840,
    "preview": "package controllers.common\n\nimport com.dimafeng.testcontainers.PostgreSQLContainer\nimport com.dimafeng.testcontainers.sc"
  },
  {
    "path": "server/src/test/scala/net/wiringbits/apis/ReCaptchaApiSpec.scala",
    "chars": 2159,
    "preview": "package net.wiringbits.apis\n\nimport net.wiringbits.common.models.Captcha\nimport net.wiringbits.config.ReCaptchaConfig\nim"
  },
  {
    "path": "server/src/test/scala/net/wiringbits/core/PostgresSpec.scala",
    "chars": 1330,
    "preview": "package net.wiringbits.core\n\nimport com.dimafeng.testcontainers.PostgreSQLContainer\nimport com.dimafeng.testcontainers.s"
  },
  {
    "path": "server/src/test/scala/net/wiringbits/core/RepositoryComponents.scala",
    "chars": 299,
    "preview": "package net.wiringbits.core\n\nimport net.wiringbits.repositories.*\nimport play.api.db.Database\n\ncase class RepositoryComp"
  },
  {
    "path": "server/src/test/scala/net/wiringbits/core/RepositorySpec.scala",
    "chars": 1188,
    "preview": "package net.wiringbits.core\n\nimport net.wiringbits.config.UserTokensConfig\nimport net.wiringbits.repositories.*\nimport o"
  },
  {
    "path": "server/src/test/scala/net/wiringbits/repositories/BackgroundJobsRepositorySpec.scala",
    "chars": 4333,
    "preview": "package net.wiringbits.repositories\n\nimport org.apache.pekko.actor.ActorSystem\nimport org.apache.pekko.stream.scaladsl.*"
  },
  {
    "path": "server/src/test/scala/net/wiringbits/repositories/UserLogsRepositorySpec.scala",
    "chars": 2082,
    "preview": "package net.wiringbits.repositories\n\nimport net.wiringbits.core.RepositorySpec\nimport org.scalatest.concurrent.ScalaFutu"
  },
  {
    "path": "server/src/test/scala/net/wiringbits/repositories/UserTokensRepositorySpec.scala",
    "chars": 2660,
    "preview": "package net.wiringbits.repositories\n\nimport net.wiringbits.core.RepositorySpec\nimport org.scalatest.OptionValues.convert"
  },
  {
    "path": "server/src/test/scala/net/wiringbits/repositories/UsersRepositorySpec.scala",
    "chars": 9168,
    "preview": "package net.wiringbits.repositories\n\nimport org.apache.pekko.actor.ActorSystem\nimport org.apache.pekko.stream.scaladsl.S"
  },
  {
    "path": "server/src/test/scala/net/wiringbits/util/DelayGeneratorSpec.scala",
    "chars": 448,
    "preview": "package net.wiringbits.util\n\nimport org.scalatest.matchers.must.Matchers.{be, must}\nimport org.scalatest.wordspec.AnyWor"
  },
  {
    "path": "server/src/test/scala/net/wiringbits/util/TokensHelperSpec.scala",
    "chars": 1219,
    "preview": "package net.wiringbits.util\n\nimport org.scalatest.matchers.must.Matchers.{be, empty, must, mustNot}\nimport org.scalatest"
  },
  {
    "path": "server/src/test/scala/utils/Executors.scala",
    "chars": 480,
    "preview": "package utils\n\nimport net.wiringbits.executors.DatabaseExecutionContext\n\nimport scala.concurrent.ExecutionContext\n\nobjec"
  },
  {
    "path": "server/src/test/scala/utils/LoginUtils.scala",
    "chars": 1800,
    "preview": "package utils\n\nimport net.wiringbits.api.ApiClient\nimport net.wiringbits.api.models.auth.Login\nimport net.wiringbits.api"
  },
  {
    "path": "server/src/test/scala/utils/RepositoryUtils.scala",
    "chars": 3061,
    "preview": "package utils\n\nimport net.wiringbits.common.models.{Email, Name}\nimport net.wiringbits.core.RepositoryComponents\nimport "
  },
  {
    "path": "web/src/main/js/index.css",
    "chars": 91,
    "preview": "html,\nbody,\n#root {\n    min-height: 100vh;\n    display: flex;\n    flex-direction: column;\n}"
  },
  {
    "path": "web/src/main/js/index.html",
    "chars": 1228,
    "preview": "<!DOCTYPE html>\n<html lang=\"en\">\n\n<head>\n  <meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\">\n  <meta charset=\"utf-8\""
  },
  {
    "path": "web/src/main/scala/net/wiringbits/API.scala",
    "chars": 768,
    "preview": "package net.wiringbits\n\nimport net.wiringbits.api.ApiClient\nimport net.wiringbits.services.StorageService\nimport org.sca"
  },
  {
    "path": "web/src/main/scala/net/wiringbits/App.scala",
    "chars": 811,
    "preview": "package net.wiringbits\n\nimport com.olvind.mui.muiMaterial.components.ThemeProvider\nimport com.olvind.mui.muiMaterial.com"
  },
  {
    "path": "web/src/main/scala/net/wiringbits/AppContext.scala",
    "chars": 968,
    "preview": "package net.wiringbits\n\nimport monix.reactive.subjects.Var\nimport net.wiringbits.common.models.Email\nimport net.wiringbi"
  },
  {
    "path": "web/src/main/scala/net/wiringbits/AppRouter.scala",
    "chars": 2926,
    "preview": "package net.wiringbits\n\nimport net.wiringbits.components.pages.*\nimport net.wiringbits.components.widgets.{AppBar, Foote"
  },
  {
    "path": "web/src/main/scala/net/wiringbits/AppTheme.scala",
    "chars": 1299,
    "preview": "package net.wiringbits\n\nimport com.olvind.mui.muiMaterial.stylesCreateThemeMod.ThemeOptions\nimport com.olvind.mui.muiMat"
  },
  {
    "path": "web/src/main/scala/net/wiringbits/I18nMessages.scala",
    "chars": 4344,
    "preview": "package net.wiringbits\n\nimport net.wiringbits.common.models.Name\nimport net.wiringbits.core.I18nLang\nimport net.wiringbi"
  },
  {
    "path": "web/src/main/scala/net/wiringbits/Main.scala",
    "chars": 1191,
    "preview": "package net.wiringbits\n\nimport monix.reactive.subjects.Var\nimport net.wiringbits.common.models.Email\nimport net.wiringbi"
  },
  {
    "path": "web/src/main/scala/net/wiringbits/components/AppSplash.scala",
    "chars": 1869,
    "preview": "package net.wiringbits.components\n\nimport net.wiringbits.AppContext\nimport net.wiringbits.core.I18nHooks\nimport net.wiri"
  },
  {
    "path": "web/src/main/scala/net/wiringbits/components/pages/AboutPage.scala",
    "chars": 1449,
    "preview": "package net.wiringbits.components.pages\n\nimport com.olvind.mui.muiMaterial.components as mui\nimport net.wiringbits.AppCo"
  }
]

// ... and 47 more files (download for full content)

About this extraction

This page contains the full source code of the wiringbits/scala-webapp-template GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 247 files (401.7 KB), approximately 104.3k tokens, and a symbol index with 11 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!