├── .bin
├── .firebaserc
├── project
│ └── build.properties
├── setup
│ ├── project
│ │ ├── build.properties
│ │ └── plugins.sbt
│ ├── src
│ │ └── main
│ │ │ ├── scala
│ │ │ ├── common
│ │ │ │ ├── SeqImplicit.scala
│ │ │ │ └── StringImplicit.scala
│ │ │ ├── arguments
│ │ │ │ ├── booleanarg
│ │ │ │ │ ├── BooleanArgValidations.scala
│ │ │ │ │ └── BooleanArgRetriever.scala
│ │ │ │ ├── stringarg
│ │ │ │ │ ├── StringArgRetriever.scala
│ │ │ │ │ └── StringArgValidations.scala
│ │ │ │ ├── stringlistarg
│ │ │ │ │ └── StringListArgRetriever.scala
│ │ │ │ └── Args.scala
│ │ │ ├── packagedomain
│ │ │ │ ├── scalafiles
│ │ │ │ │ ├── ScalaDomainFile.scala
│ │ │ │ │ ├── DomainModelId.scala
│ │ │ │ │ ├── DomainModel.scala
│ │ │ │ │ ├── SlickMapper.scala
│ │ │ │ │ ├── Service.scala
│ │ │ │ │ └── Repository.scala
│ │ │ │ ├── common
│ │ │ │ │ └── CrudHelper.scala
│ │ │ │ ├── smithyfiles
│ │ │ │ │ ├── ApiManifest.scala
│ │ │ │ │ └── SmithyDomainFile.scala
│ │ │ │ ├── DomainFile.scala
│ │ │ │ └── PackageDomain.scala
│ │ │ ├── config
│ │ │ │ └── SetupConfig.scala
│ │ │ └── Setup.scala
│ │ │ └── resources
│ │ │ └── bootstrap-conf.json
│ ├── setup.sh
│ ├── .scalafmt.conf
│ └── build.sbt
├── firebase-data
│ ├── auth_export
│ │ ├── config.json
│ │ └── accounts.json
│ └── firebase-export-metadata.json
├── swagger
│ ├── base.swagger.json
│ ├── openapi-merge.json
│ └── merge.sh
├── firebase.json
├── schemaspy
│ └── schemaspy.properties
├── mock
│ ├── mock.sh
│ └── docker-compose.yaml
├── docker-compose.yml
├── runForDoc.md
├── test-local.bat
├── test-local.sh
└── .gitignore
├── doc-assets
└── .gitkeep
├── project
├── build.properties
├── DbConf.scala
├── GithubConfig.scala
├── Build.scala
└── plugins.sbt
├── modules
├── api-definition
│ ├── project
│ │ ├── build.properties
│ │ ├── Dependencies.scala
│ │ ├── plugins.sbt
│ │ └── GithubConfig.scala
│ ├── smithy-build.json
│ ├── src
│ │ └── main
│ │ │ └── resources
│ │ │ └── META-INF
│ │ │ └── smithy
│ │ │ ├── manifest
│ │ │ ├── base.smithy
│ │ │ ├── healthAPIController.smithy
│ │ │ ├── actorSystemAPIController.smithy
│ │ │ └── actorShardingAPIController.smithy
│ └── build.sbt
├── flyway
│ └── build.sbt
└── slick
│ └── src
│ └── main
│ ├── scala
│ └── db
│ │ └── codegen
│ │ └── CustomizedCodeGenerator.scala
│ └── resources
│ └── logback.xml
├── .git-blame-ignore-revs
├── doc
├── mark_as_source_root.png
├── RepositoryDoc.md
├── ControllerDoc.md
├── changes.md
├── DaoDoc.md
├── EndpointsAuth.md
├── ModuleDocs.md
├── Deployment.md
├── FilterDoc.md
└── innFactoryIcon.svg
├── .scala-steward.conf
├── app
└── de
│ └── innfactory
│ └── bootstrapplay2
│ ├── users
│ ├── application
│ │ ├── UserController.scala
│ │ └── models
│ │ │ ├── ClaimsResponse.scala
│ │ │ ├── UserPasswordResetRequest.scala
│ │ │ ├── UserRequest.scala
│ │ │ └── UserPasswordResetTokenResponse.scala
│ ├── domain
│ │ ├── models
│ │ │ ├── UserId.scala
│ │ │ ├── UserPasswordReset.scala
│ │ │ ├── Claims.scala
│ │ │ ├── User.scala
│ │ │ └── UserPasswordResetToken.scala
│ │ └── interfaces
│ │ │ ├── UserPasswordResetTokenRepository.scala
│ │ │ ├── UserRepository.scala
│ │ │ └── UserService.scala
│ └── infrastructure
│ │ ├── mappers
│ │ ├── ClaimMapper.scala
│ │ ├── UserPasswordResetTokenMapper.scala
│ │ ├── KeycloakUserMapper.scala
│ │ └── UserRecordMapper.scala
│ │ ├── SlickUserResetTokenRepository.scala
│ │ └── UserRepositoryMock.scala
│ ├── commons
│ ├── jwt
│ │ ├── JWKUrl.scala
│ │ ├── AWSJWTValidator.scala
│ │ ├── JWTValidatorBase.scala
│ │ ├── algorithm
│ │ │ └── JWTAlgorithm.scala
│ │ ├── JWTValidator.scala
│ │ └── ConfigurableJWTValidator.scala
│ ├── implicits
│ │ └── EitherImplicits.scala
│ ├── application
│ │ └── actions
│ │ │ └── utils
│ │ │ ├── UserUtils.scala
│ │ │ └── UserUtilsImpl.scala
│ ├── filteroptions
│ │ ├── FilterOptionsConfig.scala
│ │ └── FilterOptionUtils.scala
│ ├── jackson
│ │ ├── JsValueDeSerializerModule.scala
│ │ └── playjson
│ │ │ ├── JsValueSerializer.scala
│ │ │ └── JsValueDeserializer.scala
│ ├── infrastructure
│ │ └── DatabaseHealthSocket.scala
│ ├── firebase
│ │ └── FirebaseBase.scala
│ ├── results
│ │ └── ErrorResponseWithAdditionalBody.scala
│ ├── logging
│ │ └── LoggingEnhancer.scala
│ ├── RequestContext.scala
│ └── errors
│ │ └── ErrorHandler.scala
│ ├── websockets
│ ├── domain
│ │ ├── interfaces
│ │ │ ├── WebSocketService.scala
│ │ │ └── WebSocketRepository.scala
│ │ └── DomainWebSocketService.scala
│ ├── infrastructure
│ │ └── actors
│ │ │ ├── WebSocketActorCreator.scala
│ │ │ ├── ScheduledActor.scala
│ │ │ └── WebSocketActor.scala
│ └── application
│ │ └── WebsocketController.scala
│ ├── graphql
│ ├── schema
│ │ ├── models
│ │ │ ├── Arguments.scala
│ │ │ ├── Locations.scala
│ │ │ └── Companies.scala
│ │ ├── mutations
│ │ │ └── MutationDefinition.scala
│ │ ├── queries
│ │ │ ├── QueryDefinition.scala
│ │ │ └── Company.scala
│ │ └── SchemaDefinition.scala
│ ├── ExecutionServices.scala
│ ├── GraphQLExecutionContext.scala
│ ├── ErrorParserImpl.scala
│ ├── GraphQLController.scala
│ └── RequestExecutor.scala
│ ├── application
│ ├── keycloak
│ │ ├── domain
│ │ │ └── models
│ │ │ │ ├── KeycloakCredentials.scala
│ │ │ │ ├── KeycloakRoles.scala
│ │ │ │ ├── KeycloakUserCreation.scala
│ │ │ │ └── KeycloakUser.scala
│ │ └── infrastructure
│ │ │ └── KeycloakOAuthRepository.scala
│ └── controller
│ │ ├── HealthController.scala
│ │ ├── BaseMapper.scala
│ │ └── BaseController.scala
│ ├── actorsystem
│ ├── domain
│ │ ├── interfaces
│ │ │ └── HelloWorldService.scala
│ │ ├── commands
│ │ │ └── Commands.scala
│ │ └── services
│ │ │ └── HelloWorldServiceImpl.scala
│ └── application
│ │ └── ActorSystemHelloWorldController.scala
│ ├── actorsharding
│ ├── domain
│ │ ├── interfaces
│ │ │ └── HelloWorldService.scala
│ │ ├── common
│ │ │ └── Sharding.scala
│ │ └── services
│ │ │ └── HelloWorldServiceImpl.scala
│ └── application
│ │ └── ActorShardingHelloWorldController.scala
│ ├── companies
│ ├── domain
│ │ ├── models
│ │ │ ├── Company.scala
│ │ │ └── CompanyId.scala
│ │ ├── interfaces
│ │ │ ├── CompanyRepository.scala
│ │ │ └── CompanyService.scala
│ │ └── services
│ │ │ └── DomainCompanyService.scala
│ ├── infrastructure
│ │ └── mapper
│ │ │ └── CompanyMapper.scala
│ └── application
│ │ ├── mapper
│ │ └── CompanyMapper.scala
│ │ └── CompanyController.scala
│ ├── filters
│ ├── MiddlewareRegistry.scala
│ ├── access
│ │ └── RouteBlacklistFilter.scala
│ └── logging
│ │ └── AccessLoggingFilter.scala
│ └── locations
│ ├── domain
│ ├── models
│ │ ├── LocationId.scala
│ │ └── Location.scala
│ ├── interfaces
│ │ ├── LocationRepository.scala
│ │ └── LocationService.scala
│ └── services
│ │ └── DomainLocationService.scala
│ ├── infrastructure
│ └── mapper
│ │ └── LocationMapper.scala
│ └── application
│ └── mapper
│ └── LocationMapper.scala
├── test
├── testutils
│ ├── models
│ │ └── ErrorResponseBody.scala
│ ├── DateTimeUtil.scala
│ ├── grapqhl
│ │ ├── FakeGraphQLRequest.scala
│ │ └── CompanyRequests.scala
│ ├── BaseFakeRequest.scala
│ ├── AuthUtils.scala
│ └── FakeRequestClient.scala
├── controllers
│ ├── CompaniesGraphqlControllerTest.scala
│ ├── HealthControllerTest.scala
│ ├── TestApplicationFactory.scala
│ ├── ActorControllerTest.scala
│ └── LocationsControllerTest.scala
└── resources
│ ├── application.conf
│ ├── logback.xml
│ └── migration
│ └── V999__DATA.sql
├── conf
├── dev-application.conf
├── routes
├── akka.conf
├── db
│ └── migration
│ │ └── V1__Tables.sql
├── logback.xml
├── logback-local.xml
└── application.conf
├── .mergify.yml
├── .deployment
├── install-aws-iam-authenticator.sh
└── deployment.tpl.yaml
├── .scalafmt.conf
├── .gitignore
└── .github
└── workflows
├── openapi-yaml-gen.yml
└── test.yml
/.bin/.firebaserc:
--------------------------------------------------------------------------------
1 | {}
--------------------------------------------------------------------------------
/doc-assets/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/project/build.properties:
--------------------------------------------------------------------------------
1 | sbt.version = 1.9.9
--------------------------------------------------------------------------------
/.bin/project/build.properties:
--------------------------------------------------------------------------------
1 | sbt.version=1.4.1
2 |
--------------------------------------------------------------------------------
/.bin/setup/project/build.properties:
--------------------------------------------------------------------------------
1 | sbt.version=1.6.2
2 |
--------------------------------------------------------------------------------
/modules/api-definition/project/build.properties:
--------------------------------------------------------------------------------
1 | sbt.version=1.6.2
2 |
--------------------------------------------------------------------------------
/.bin/firebase-data/auth_export/config.json:
--------------------------------------------------------------------------------
1 | {"signIn":{"allowDuplicateEmails":false}}
--------------------------------------------------------------------------------
/modules/flyway/build.sbt:
--------------------------------------------------------------------------------
1 | flywayLocations := Seq("filesystem:conf/db/migration")
2 |
--------------------------------------------------------------------------------
/.bin/setup/project/plugins.sbt:
--------------------------------------------------------------------------------
1 | addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "2.0.0-RC1")
2 |
--------------------------------------------------------------------------------
/.git-blame-ignore-revs:
--------------------------------------------------------------------------------
1 | # Scala Steward: Reformat with scalafmt 3.7.15
2 | 8692eba8e19ddef6ff7e7407918f95527b83719d
3 |
--------------------------------------------------------------------------------
/doc/mark_as_source_root.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/innFactory/bootstrap-play2/HEAD/doc/mark_as_source_root.png
--------------------------------------------------------------------------------
/project/DbConf.scala:
--------------------------------------------------------------------------------
1 | case class DbConf(profile: String, driver: String, user: String, password: String, url: String)
2 |
--------------------------------------------------------------------------------
/modules/api-definition/project/Dependencies.scala:
--------------------------------------------------------------------------------
1 | import sbt._
2 |
3 | object Dependencies {
4 | val scalaVersion = "2.13.13"
5 | }
6 |
--------------------------------------------------------------------------------
/.scala-steward.conf:
--------------------------------------------------------------------------------
1 | scalafmt.runAfterUpgrading = true
2 | commits.message = "chore: update ${artifactName} from ${currentVersion} to ${nextVersion}"
3 |
--------------------------------------------------------------------------------
/.bin/swagger/base.swagger.json:
--------------------------------------------------------------------------------
1 | {
2 | "openapi": "3.0.2",
3 | "info": {
4 | "title": "API",
5 | "version": "1.0.0"
6 | },
7 | "paths": {}
8 | }
9 |
--------------------------------------------------------------------------------
/app/de/innfactory/bootstrapplay2/users/application/UserController.scala:
--------------------------------------------------------------------------------
1 | package de.innfactory.bootstrapplay2.users.application
2 |
3 | class UserController {}
4 |
--------------------------------------------------------------------------------
/.bin/firebase-data/firebase-export-metadata.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": "9.10.0",
3 | "auth": {
4 | "version": "9.10.0",
5 | "path": "auth_export"
6 | }
7 | }
--------------------------------------------------------------------------------
/app/de/innfactory/bootstrapplay2/users/domain/models/UserId.scala:
--------------------------------------------------------------------------------
1 | package de.innfactory.bootstrapplay2.users.domain.models
2 |
3 | case class UserId(value: String)
4 |
--------------------------------------------------------------------------------
/.bin/swagger/openapi-merge.json:
--------------------------------------------------------------------------------
1 | {
2 | "inputs": [
3 | {
4 | "inputFile": ".bin/swagger/base.swagger.json"
5 | }
6 | ],
7 | "output": "./output.swagger.json"
8 | }
9 |
--------------------------------------------------------------------------------
/modules/api-definition/smithy-build.json:
--------------------------------------------------------------------------------
1 | {
2 | "imports": ["$PWD"],
3 | "mavenDependencies": [
4 | "com.disneystreaming.smithy4s:smithy4s-protocol_2.13:latest.stable"
5 | ]
6 | }
7 |
--------------------------------------------------------------------------------
/modules/api-definition/project/plugins.sbt:
--------------------------------------------------------------------------------
1 | addSbtPlugin("com.codecommit" % "sbt-github-packages" % "0.5.3")
2 | addSbtPlugin("com.disneystreaming.smithy4s" % "smithy4s-sbt-codegen" % "0.17.19")
3 |
--------------------------------------------------------------------------------
/.bin/firebase.json:
--------------------------------------------------------------------------------
1 | {
2 | "emulators": {
3 | "auth": {
4 | "port": 9099
5 | },
6 | "ui": {
7 | "enabled": true,
8 | "port": 8099
9 | }
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/modules/api-definition/src/main/resources/META-INF/smithy/manifest:
--------------------------------------------------------------------------------
1 | actorAPIController.smithy
2 | base.smithy
3 | companyAPIController.smithy
4 | healthAPIController.smithy
5 | locationAPIController.smithy
--------------------------------------------------------------------------------
/doc/RepositoryDoc.md:
--------------------------------------------------------------------------------
1 | # Repository Documentation
2 | ######Last Updated: 17.06.2020
3 |
4 | The Repositories are used to access the DAOs, aggregate Data and check if the data can be accessed by the user.
5 |
--------------------------------------------------------------------------------
/.bin/setup/src/main/scala/common/SeqImplicit.scala:
--------------------------------------------------------------------------------
1 | package common
2 |
3 | object SeqImplicit {
4 | implicit class EmptySeqOption[A](seq: Seq[A]) {
5 | def toOption: Option[Seq[A]] = Option(seq).filter(_.nonEmpty)
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/.bin/setup/src/main/resources/bootstrap-conf.json:
--------------------------------------------------------------------------------
1 | {
2 | "project": {
3 | "name": "bootstrap-play2",
4 | "domain": "de.innfactory.bootstrapplay2"
5 | },
6 | "bootstrap": {
7 | "paths": ["conf", "test", ".github"]
8 | }
9 | }
--------------------------------------------------------------------------------
/.bin/schemaspy/schemaspy.properties:
--------------------------------------------------------------------------------
1 | schemaspy.t=pgsql
2 | schemaspy.host=localhost
3 | schemaspy.port=5432
4 | schemaspy.db=test
5 | schemaspy.u=test
6 | schemaspy.p=test
7 | schemaspy.schemas=postgis
8 | schemaspy.o=./output
9 | schemaspy.dp=./
--------------------------------------------------------------------------------
/.bin/mock/mock.sh:
--------------------------------------------------------------------------------
1 | .bin/swagger/merge.sh && docker run --rm -v "${PWD}:/local" openapitools/openapi-generator-cli generate \
2 | -i /local/output.swagger.json \
3 | -g openapi-yaml \
4 | -o /local/.bin/mock/yaml && cd .bin/mock && docker-compose up
--------------------------------------------------------------------------------
/.bin/setup/src/main/scala/common/StringImplicit.scala:
--------------------------------------------------------------------------------
1 | package common
2 |
3 | object StringImplicit {
4 | implicit class EmptyStringOption(string: String) {
5 | def toOption: Option[String] = Option(string).filter(_.trim.nonEmpty)
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/app/de/innfactory/bootstrapplay2/users/domain/models/UserPasswordReset.scala:
--------------------------------------------------------------------------------
1 | package de.innfactory.bootstrapplay2.users.domain.models
2 |
3 | case class UserPasswordReset(
4 | userId: UserId,
5 | password: String,
6 | token: String
7 | )
8 |
--------------------------------------------------------------------------------
/doc/ControllerDoc.md:
--------------------------------------------------------------------------------
1 | # Controller Documentation
2 | ###### Last Updated: 17.06.2020
3 |
4 | Official Play Documentation: [Play Actions, Controllers, Results Documentation 2.8](https://www.playframework.com/documentation/2.8.x/ScalaActions)
5 |
6 |
--------------------------------------------------------------------------------
/app/de/innfactory/bootstrapplay2/users/domain/models/Claims.scala:
--------------------------------------------------------------------------------
1 | package de.innfactory.bootstrapplay2.users.domain.models
2 |
3 | case class Claims(
4 | innFactoryAdmin: Option[Boolean] = None,
5 | companyAdmin: Option[Long] = None
6 | )
7 |
8 | object Claims {}
9 |
--------------------------------------------------------------------------------
/doc/changes.md:
--------------------------------------------------------------------------------
1 | ## Version 2.1.0
2 |
3 | > 12.08.2020
4 |
5 | Upgrade to Scala 2.13.3 and latest lib versions.
6 |
7 |
8 | ## Version 2.0.0
9 |
10 | > 15.05.2020
11 |
12 | First stable Version
13 |
14 | ## Version 1.X.X
15 |
16 | > XX.XX.2018
17 |
18 | Legacy Version
--------------------------------------------------------------------------------
/test/testutils/models/ErrorResponseBody.scala:
--------------------------------------------------------------------------------
1 | package testutils.models
2 |
3 | import play.api.libs.json.Json
4 |
5 | case class ErrorResponseBody(message: String)
6 |
7 | object ErrorResponseBody {
8 | implicit val format = Json.format[ErrorResponseBody]
9 | }
10 |
--------------------------------------------------------------------------------
/conf/dev-application.conf:
--------------------------------------------------------------------------------
1 | include "application.conf"
2 |
3 | akka.cluster.seed-nodes = [ "akka://application@127.0.0.1:25520" ]
4 | akka {
5 | remote {
6 | artery {
7 | transport = tcp # See Selecting a transport below
8 | canonical.hostname = "127.0.0.1"
9 | canonical.port = 25520
10 | }
11 | }
12 | }
--------------------------------------------------------------------------------
/.bin/mock/docker-compose.yaml:
--------------------------------------------------------------------------------
1 | version: '3.1'
2 |
3 | services:
4 | mock:
5 | container_name: mock-server
6 | image: stoplight/prism:4.10.3
7 | command: >
8 | mock -p 4010 --host 0.0.0.0 -m false /root/apis/openapi.yaml
9 | ports:
10 | - 9003:4010
11 | volumes:
12 | - ./yaml/openapi:/root/apis
13 |
--------------------------------------------------------------------------------
/app/de/innfactory/bootstrapplay2/commons/jwt/JWKUrl.scala:
--------------------------------------------------------------------------------
1 | package de.innfactory.bootstrapplay2.commons.jwt
2 |
3 | import play.api.libs.json.Json
4 |
5 | final case class JWKUrl(value: String) extends AnyVal
6 |
7 | object JWKUrl {
8 | implicit val reads = Json.reads[JWKUrl]
9 | implicit val writes = Json.writes[JWKUrl]
10 | }
11 |
--------------------------------------------------------------------------------
/test/testutils/DateTimeUtil.scala:
--------------------------------------------------------------------------------
1 | package testutils
2 |
3 | import org.joda.time.{DateTime, DateTimeUtils}
4 |
5 | object DateTimeUtil {
6 | def setToDateTime(dateTime: String): Unit = DateTimeUtils.setCurrentMillisFixed(new DateTime(dateTime).getMillis)
7 |
8 | def resetDateTime(): Unit = DateTimeUtils.setCurrentMillisSystem()
9 | }
10 |
--------------------------------------------------------------------------------
/.bin/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: '3.1'
2 |
3 | services:
4 | db:
5 | container_name: bootstrapPlay2PG
6 | image: postgres:13
7 | environment:
8 | POSTGRES_PASSWORD: test
9 | POSTGRES_DB: test
10 | POSTGRES_USER: test
11 | ports:
12 | - 5432:5432
13 | volumes:
14 | - ./postgis-volume/data:/var/lib/postgresql/data
--------------------------------------------------------------------------------
/.bin/setup/setup.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | PATH_TO_SETUP_JAR=$(dirname "$0")"/target/scala-2.13/Setup-assembly-0.0.1.jar"
4 |
5 | if [ ! -f "$PATH_TO_SETUP_JAR" ]; then
6 | echo "$PATH_TO_SETUP_JAR does not exist."
7 | echo "Creating..."
8 | (cd $(dirname "$0"); sbt assembly)
9 | :
10 |
11 | fi
12 | exec scala "$PATH_TO_SETUP_JAR" "$@"
13 |
--------------------------------------------------------------------------------
/modules/api-definition/src/main/resources/META-INF/smithy/base.smithy:
--------------------------------------------------------------------------------
1 | $version: "2.0"
2 |
3 | namespace de.innfactory.bootstrapplay2.api
4 | use alloy#uuidFormat
5 |
6 | @pattern("^\\d{4}-\\d\\d-\\d\\dT\\d\\d:\\d\\d:\\d\\d(\\.\\d+)?(([+-]\\d\\d:\\d\\d)|Z)?$")
7 | @documentation("ISO Date With Time")
8 | string DateWithTime
9 |
10 | string CompanyId
11 | string LocationId
12 |
--------------------------------------------------------------------------------
/.mergify.yml:
--------------------------------------------------------------------------------
1 | pull_request_rules:
2 | - name: Automatic merge on successful tests by PR from jona7o (Scala Steward)
3 | conditions:
4 | - author=jona7o
5 | - status-success~=test
6 | actions:
7 | merge:
8 | method: merge
9 | - name: delete head branch after merge
10 | conditions:
11 | - merged
12 | actions:
13 | delete_head_branch: {}
14 |
15 |
--------------------------------------------------------------------------------
/modules/api-definition/build.sbt:
--------------------------------------------------------------------------------
1 | val releaseVersion = "0.0.1"
2 |
3 | lazy val apiDefinition = (project in file("."))
4 | .enablePlugins(Smithy4sCodegenPlugin)
5 | .settings(
6 | name := "api-definition",
7 | scalaVersion := Dependencies.scalaVersion,
8 | organization := "de.innfactory.bootstrap-play2",
9 | version := releaseVersion,
10 | GithubConfig.settings
11 | )
12 |
--------------------------------------------------------------------------------
/app/de/innfactory/bootstrapplay2/users/application/models/ClaimsResponse.scala:
--------------------------------------------------------------------------------
1 | package de.innfactory.bootstrapplay2.users.application.models
2 |
3 | import play.api.libs.json.Json
4 |
5 | case class ClaimsResponse(
6 | innFactoryAdmin: Option[Boolean] = None,
7 | companyAdmin: Option[Boolean] = None
8 | )
9 |
10 | object ClaimsResponse {
11 | implicit val format = Json.format[ClaimsResponse]
12 | }
13 |
--------------------------------------------------------------------------------
/doc/DaoDoc.md:
--------------------------------------------------------------------------------
1 | # DAO Documentation
2 | ###### Last Updated: 17.06.202
3 |
4 | ## Description
5 |
6 | DAOs are used to access the database and create, read, update or delete the data.
7 |
8 | ## BaseSlickDAO
9 |
10 | The **BaseSlickDAO** provides basic CRUD Operations.
11 | A newly created DAO has to extend from the BaseSlickDAO to use the functionality.
12 | Two Demo DAOs can be found in __./app/db/*__ .
13 |
--------------------------------------------------------------------------------
/.bin/setup/src/main/scala/arguments/booleanarg/BooleanArgValidations.scala:
--------------------------------------------------------------------------------
1 | package arguments.booleanarg
2 |
3 | object BooleanArgValidations {
4 | def isBooleanString(booleanString: String): Either[String, Boolean] = booleanString.toLowerCase match {
5 | case "y" | "yes" => Right(true)
6 | case "n" | "no" => Right(false)
7 | case _ => Left("Invalid! Valid: y | yes | n | no")
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/app/de/innfactory/bootstrapplay2/websockets/domain/interfaces/WebSocketService.scala:
--------------------------------------------------------------------------------
1 | package de.innfactory.bootstrapplay2.websockets.domain.interfaces
2 |
3 | import akka.stream.scaladsl.Flow
4 | import com.google.inject.ImplementedBy
5 | import de.innfactory.bootstrapplay2.websockets.domain.DomainWebSocketService
6 |
7 | @ImplementedBy(classOf[DomainWebSocketService])
8 | trait WebSocketService {
9 | def socket: Flow[Any, Nothing, _]
10 | }
11 |
--------------------------------------------------------------------------------
/app/de/innfactory/bootstrapplay2/graphql/schema/models/Arguments.scala:
--------------------------------------------------------------------------------
1 | package de.innfactory.bootstrapplay2.graphql.schema.models
2 |
3 | import sangria.schema.{Argument, OptionInputType, StringType}
4 |
5 | object Arguments {
6 | val FilterArg: Argument[Option[String]] =
7 | Argument(
8 | "filter",
9 | OptionInputType(StringType),
10 | description = "Filters for companies, separated by & with key=value"
11 | )
12 | }
13 |
--------------------------------------------------------------------------------
/app/de/innfactory/bootstrapplay2/graphql/ExecutionServices.scala:
--------------------------------------------------------------------------------
1 | package de.innfactory.bootstrapplay2.graphql
2 |
3 | import de.innfactory.bootstrapplay2.companies.domain.interfaces.CompanyService
4 | import de.innfactory.bootstrapplay2.locations.domain.interfaces.LocationService
5 |
6 | import javax.inject.Inject
7 |
8 | case class ExecutionServices @Inject() (
9 | companiesService: CompanyService,
10 | locationsService: LocationService
11 | )
12 |
--------------------------------------------------------------------------------
/app/de/innfactory/bootstrapplay2/websockets/infrastructure/actors/WebSocketActorCreator.scala:
--------------------------------------------------------------------------------
1 | package de.innfactory.bootstrapplay2.websockets.infrastructure.actors
2 |
3 | import akka.actor.{ActorRef, Props}
4 | import de.innfactory.bootstrapplay2.websockets.domain.interfaces.WebSocketRepository
5 |
6 | class WebSocketActorCreator extends WebSocketRepository {
7 | def createWebSocketActor(out: ActorRef): Props =
8 | WebSocketActor.props(out)
9 | }
10 |
--------------------------------------------------------------------------------
/.bin/setup/src/main/scala/packagedomain/scalafiles/ScalaDomainFile.scala:
--------------------------------------------------------------------------------
1 | package packagedomain.scalafiles
2 |
3 | import config.SetupConfig
4 | import packagedomain.DomainFile
5 |
6 | trait ScalaDomainFile extends DomainFile {
7 | val fileEnding = "scala"
8 |
9 | override protected def getFullDirectoryPath()(implicit config: SetupConfig): String =
10 | s"${System.getProperty("user.dir")}/${config.project.getPackagePath()}${getAdjustedSubPath()}"
11 | }
12 |
--------------------------------------------------------------------------------
/app/de/innfactory/bootstrapplay2/websockets/domain/interfaces/WebSocketRepository.scala:
--------------------------------------------------------------------------------
1 | package de.innfactory.bootstrapplay2.websockets.domain.interfaces
2 |
3 | import akka.actor.{ActorRef, Props}
4 | import com.google.inject.ImplementedBy
5 | import de.innfactory.bootstrapplay2.websockets.infrastructure.actors.WebSocketActorCreator
6 |
7 | @ImplementedBy(classOf[WebSocketActorCreator])
8 | trait WebSocketRepository {
9 | def createWebSocketActor(out: ActorRef): Props
10 | }
11 |
--------------------------------------------------------------------------------
/.deployment/install-aws-iam-authenticator.sh:
--------------------------------------------------------------------------------
1 |
2 | #!/bin/bash
3 |
4 | echo "Downloading AWS IAM Authenticator"
5 | curl -s -S -L -o aws-iam-authenticator https://amazon-eks.s3.us-west-2.amazonaws.com/1.16.8/2020-04-16/bin/linux/amd64/aws-iam-authenticator
6 | echo "Successfully downloaded AWS IAM Authenticator"
7 | chmod +x ./aws-iam-authenticator
8 | mkdir -p $HOME/bin && cp ./aws-iam-authenticator $HOME/bin/aws-iam-authenticator && export PATH=$PATH:$HOME/bin
9 | echo "::add-path::$HOME/bin"
--------------------------------------------------------------------------------
/app/de/innfactory/bootstrapplay2/application/keycloak/domain/models/KeycloakCredentials.scala:
--------------------------------------------------------------------------------
1 | package de.innfactory.bootstrapplay2.application.keycloak.domain.models
2 |
3 | import play.api.libs.json.{Json, OFormat}
4 |
5 | case class KeycloakCredentials(
6 | value: String,
7 | `type`: String,
8 | temporary: Boolean
9 | )
10 |
11 | object KeycloakCredentials {
12 | implicit val formatCredentials: OFormat[KeycloakCredentials] = Json.format[KeycloakCredentials]
13 | }
14 |
--------------------------------------------------------------------------------
/.scalafmt.conf:
--------------------------------------------------------------------------------
1 | version=3.8.3
2 | runner.dialect=scala213
3 | maxColumn = 120
4 | docstrings.style = Asterisk
5 | lineEndings = preserve
6 | includeCurlyBraceInSelectChains = false
7 | newlines.alwaysBeforeMultilineDef = false
8 | newlines.penalizeSingleSelectMultiArgList = false
9 | align.openParenCallSite = false
10 | rewrite.rules = [SortImports, RedundantBraces, RedundantParens, PreferCurlyFors]
11 | rewrite.redundantBraces.generalExpressions = false
12 | optIn.annotationNewlines = true
13 |
--------------------------------------------------------------------------------
/app/de/innfactory/bootstrapplay2/users/application/models/UserPasswordResetRequest.scala:
--------------------------------------------------------------------------------
1 | package de.innfactory.bootstrapplay2.users.application.models
2 |
3 | import play.api.libs.json.Json
4 |
5 | case class UserPasswordResetRequest(
6 | userId: String,
7 | password: String,
8 | token: String
9 | )
10 |
11 | object UserPasswordResetRequest {
12 | implicit val writes = Json.writes[UserPasswordResetRequest]
13 | implicit val reads = Json.reads[UserPasswordResetRequest]
14 | }
15 |
--------------------------------------------------------------------------------
/.bin/setup/.scalafmt.conf:
--------------------------------------------------------------------------------
1 | version=3.5.9
2 | runner.dialect=scala213
3 | maxColumn = 120
4 | docstrings.style = Asterisk
5 | lineEndings = preserve
6 | includeCurlyBraceInSelectChains = false
7 | newlines.alwaysBeforeMultilineDef = false
8 | newlines.penalizeSingleSelectMultiArgList = false
9 | align.openParenCallSite = false
10 | rewrite.rules = [SortImports, RedundantBraces, RedundantParens, PreferCurlyFors]
11 | rewrite.redundantBraces.generalExpressions = false
12 | optIn.annotationNewlines = true
13 |
--------------------------------------------------------------------------------
/app/de/innfactory/bootstrapplay2/application/keycloak/domain/models/KeycloakRoles.scala:
--------------------------------------------------------------------------------
1 | package de.innfactory.bootstrapplay2.application.keycloak.domain.models
2 |
3 | import play.api.libs.json.{Json, OFormat}
4 |
5 | case class KeycloakRoles(
6 | id: String,
7 | name: String,
8 | composite: Boolean,
9 | clientRole: Boolean,
10 | containerId: String
11 | )
12 |
13 | object KeycloakRoles {
14 | implicit val formatKeycloakRoles: OFormat[KeycloakRoles] = Json.format[KeycloakRoles]
15 | }
16 |
--------------------------------------------------------------------------------
/.bin/setup/src/main/scala/packagedomain/common/CrudHelper.scala:
--------------------------------------------------------------------------------
1 | package packagedomain.common
2 |
3 | trait CrudHelper {
4 | protected val CrudLogicKey = "%%CRUD_LOGIC%%"
5 | protected val CrudImportsKey = "%%CRUD_IMPORTS%%"
6 |
7 | protected def replaceForCrud(content: String, withCrud: Boolean, crudLogic: String, crudImports: String): String =
8 | content
9 | .replaceAll(CrudLogicKey, if (withCrud) crudLogic else "")
10 | .replaceAll(CrudImportsKey, if (withCrud) crudImports else "")
11 | }
12 |
--------------------------------------------------------------------------------
/app/de/innfactory/bootstrapplay2/graphql/schema/mutations/MutationDefinition.scala:
--------------------------------------------------------------------------------
1 | package de.innfactory.bootstrapplay2.graphql.schema.mutations
2 |
3 | import de.innfactory.bootstrapplay2.graphql.GraphQLExecutionContext
4 | import sangria.schema.{fields, ObjectType}
5 |
6 | object MutationDefinition {
7 |
8 | val Mutations: ObjectType[GraphQLExecutionContext, Unit] = ObjectType(
9 | name = "mutation",
10 | description = "Bootstrap API Mutations",
11 | fields = fields(
12 | )
13 | )
14 |
15 | }
16 |
--------------------------------------------------------------------------------
/app/de/innfactory/bootstrapplay2/users/application/models/UserRequest.scala:
--------------------------------------------------------------------------------
1 | package de.innfactory.bootstrapplay2.users.application.models
2 |
3 | import de.innfactory.bootstrapplay2.users.domain.models.Claims
4 |
5 | case class UserRequest(
6 | userId: String,
7 | email: String,
8 | emailVerified: Boolean,
9 | disabled: Boolean,
10 | claims: Claims,
11 | displayName: Option[String],
12 | lastSignIn: Option[Long],
13 | lastRefresh: Option[Long],
14 | creation: Option[Long]
15 | ) {}
16 |
--------------------------------------------------------------------------------
/doc/EndpointsAuth.md:
--------------------------------------------------------------------------------
1 | # Endpoints Authentication/Authorization:
2 |
3 | #### /v1/companies/*:
4 |
5 | - Not Public (JWT Token)
6 | - User can create, access, update and delete company (**POST**, **GET**, **PATCH**, **DELETE**)
7 |
8 | #### /v1/locations/*:
9 |
10 | - Not Public (JWT Token)
11 | - User can create, access, update and delete location (**POST**, **GET**, **PATCH**, **DELETE**)
12 |
13 | #### /v1/public/*
14 |
15 | - Public
16 | - Only Locations will be returned in the "/distance/" Query
17 | - Actorsystem is public
--------------------------------------------------------------------------------
/app/de/innfactory/bootstrapplay2/commons/implicits/EitherImplicits.scala:
--------------------------------------------------------------------------------
1 | package de.innfactory.bootstrapplay2.commons.implicits
2 |
3 | import scala.concurrent.{ExecutionContext, Future}
4 |
5 | object EitherImplicits {
6 |
7 | implicit class EitherFuture[A, B](value: Either[A, Future[B]]) {
8 | def foldEitherOfFuture(implicit ec: ExecutionContext): Future[Either[A, B]] =
9 | value match {
10 | case Left(s) => Future.successful(Left(s))
11 | case Right(f) => f.map(Right(_))
12 | }
13 | }
14 |
15 | }
16 |
--------------------------------------------------------------------------------
/app/de/innfactory/bootstrapplay2/actorsystem/domain/interfaces/HelloWorldService.scala:
--------------------------------------------------------------------------------
1 | package de.innfactory.bootstrapplay2.actorsystem.domain.interfaces
2 |
3 | import com.google.inject.ImplementedBy
4 | import de.innfactory.bootstrapplay2.actorsystem.domain.commands.Response
5 | import de.innfactory.bootstrapplay2.actorsystem.domain.services.HelloWorldServiceImpl
6 |
7 | import scala.concurrent.Future
8 |
9 | @ImplementedBy(classOf[HelloWorldServiceImpl])
10 | trait HelloWorldService {
11 | def queryHelloWorld(query: String): Future[Response]
12 | }
13 |
--------------------------------------------------------------------------------
/app/de/innfactory/bootstrapplay2/actorsharding/domain/interfaces/HelloWorldService.scala:
--------------------------------------------------------------------------------
1 | package de.innfactory.bootstrapplay2.actorsharding.domain.interfaces
2 |
3 | import com.google.inject.ImplementedBy
4 | import de.innfactory.bootstrapplay2.actorsharding.domain.services.HelloWorldServiceImpl
5 | import de.innfactory.bootstrapplay2.actorsystem.domain.commands.Response
6 |
7 | import scala.concurrent.Future
8 |
9 | @ImplementedBy(classOf[HelloWorldServiceImpl])
10 | trait HelloWorldService {
11 | def queryHelloWorld(query: String): Future[Response]
12 | }
13 |
--------------------------------------------------------------------------------
/.bin/setup/src/main/scala/arguments/booleanarg/BooleanArgRetriever.scala:
--------------------------------------------------------------------------------
1 | package arguments.booleanarg
2 |
3 | import scala.annotation.tailrec
4 | import scala.io.StdIn.readLine
5 |
6 | object BooleanArgRetriever {
7 | @tailrec
8 | def askFor(message: String): Boolean = {
9 | println(s"$message (y/n)")
10 | val is = readLine().toLowerCase()
11 | BooleanArgValidations.isBooleanString(is) match {
12 | case Left(error) =>
13 | println(error)
14 | askFor(message)
15 | case Right(boolean) => boolean
16 | }
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/app/de/innfactory/bootstrapplay2/graphql/schema/queries/QueryDefinition.scala:
--------------------------------------------------------------------------------
1 | package de.innfactory.bootstrapplay2.graphql.schema.queries
2 |
3 | import Company.allCompanies
4 | import de.innfactory.bootstrapplay2.graphql.GraphQLExecutionContext
5 | import sangria.schema.{fields, ObjectType}
6 |
7 | object QueryDefinition {
8 | val Query: ObjectType[GraphQLExecutionContext, Unit] = ObjectType(
9 | name = "Query",
10 | description = "Bootstrap API Queries",
11 | fields = fields[GraphQLExecutionContext, Unit](
12 | allCompanies
13 | )
14 | )
15 | }
16 |
--------------------------------------------------------------------------------
/app/de/innfactory/bootstrapplay2/graphql/GraphQLExecutionContext.scala:
--------------------------------------------------------------------------------
1 | package de.innfactory.bootstrapplay2.graphql
2 |
3 | import play.api.mvc.{AnyContent, Request}
4 | import de.innfactory.bootstrapplay2.companies.domain.interfaces.CompanyService
5 | import de.innfactory.bootstrapplay2.locations.domain.interfaces.LocationService
6 |
7 | import scala.concurrent.ExecutionContext
8 |
9 | case class GraphQLExecutionContext(
10 | request: Request[AnyContent],
11 | ec: ExecutionContext,
12 | companiesService: CompanyService,
13 | locationsService: LocationService
14 | )
15 |
--------------------------------------------------------------------------------
/app/de/innfactory/bootstrapplay2/commons/application/actions/utils/UserUtils.scala:
--------------------------------------------------------------------------------
1 | package de.innfactory.bootstrapplay2.commons.application.actions.utils
2 |
3 | import com.google.inject.ImplementedBy
4 | import de.innfactory.play.results.Results.Result
5 | import de.innfactory.play.smithy4play.JWTToken
6 |
7 | import scala.concurrent.Future
8 |
9 | @ImplementedBy(classOf[UserUtilsImpl])
10 | trait UserUtils[USER] {
11 | def validateJwtToken(authorizationHeader: Option[JWTToken]): Future[Result[Unit]]
12 | def getUser(authorizationHeader: Option[JWTToken]): Future[Result[USER]]
13 | }
14 |
--------------------------------------------------------------------------------
/app/de/innfactory/bootstrapplay2/commons/filteroptions/FilterOptionsConfig.scala:
--------------------------------------------------------------------------------
1 | package de.innfactory.bootstrapplay2.commons.filteroptions
2 |
3 | import dbdata.Tables
4 | import de.innfactory.play.slick.enhanced.utils.filteroptions.{
5 | BooleanOption,
6 | FilterOptions,
7 | LongOption,
8 | OptionStringOption
9 | }
10 |
11 | class FilterOptionsConfig {
12 |
13 | val companiesFilterOptions: Seq[FilterOptions[Tables.Company, _]] = Seq(
14 | OptionStringOption(v => v.stringAttribute1, "stringAttribute1"),
15 | OptionStringOption(v => v.stringAttribute2, "stringAttribute2")
16 | )
17 |
18 | }
19 |
--------------------------------------------------------------------------------
/modules/api-definition/src/main/resources/META-INF/smithy/healthAPIController.smithy:
--------------------------------------------------------------------------------
1 | $version: "2.0"
2 |
3 | namespace de.innfactory.bootstrapplay2.api
4 |
5 | use alloy#simpleRestJson
6 |
7 | @simpleRestJson
8 | service HealthAPIController {
9 | version: "1.0.0",
10 | operations: [ping, liveness, readiness]
11 | }
12 |
13 | @http(method: "GET", uri: "/", code: 200)
14 | @readonly
15 | operation ping {}
16 |
17 | @http(method: "GET", uri: "/liveness", code: 200)
18 | @readonly
19 | operation liveness {}
20 |
21 | @http(method: "GET", uri: "/readiness", code: 200)
22 | @readonly
23 | operation readiness {}
--------------------------------------------------------------------------------
/project/GithubConfig.scala:
--------------------------------------------------------------------------------
1 | import sbt.Credentials
2 | import sbt.Keys.credentials
3 | import sbtghpackages.GitHubPackagesPlugin.autoImport.{githubOwner, githubRepository}
4 |
5 | object GithubConfig {
6 | private val token = sys.env.getOrElse("GITHUB_TOKEN", "")
7 |
8 | val settings = Seq(
9 | githubOwner := "innFactory",
10 | githubRepository := "bootstrap-play2",
11 | credentials :=
12 | Seq(
13 | Credentials(
14 | "GitHub Package Registry",
15 | "maven.pkg.github.com",
16 | "innFactory",
17 | token
18 | )
19 | )
20 | )
21 | }
22 |
--------------------------------------------------------------------------------
/.bin/setup/src/main/scala/packagedomain/smithyfiles/ApiManifest.scala:
--------------------------------------------------------------------------------
1 | package packagedomain.smithyfiles
2 | import config.SetupConfig
3 |
4 | case class ApiManifest(packageDomain: String, packageName: String) extends SmithyDomainFile {
5 | override def subPath: String = ""
6 | override def name: String = "manifest"
7 |
8 | override val fileEnding: String = ""
9 |
10 | override protected def getContent(withCrud: Boolean)(implicit config: SetupConfig): String = {
11 | val apiDefinition = ApiDefinition(packageDomain, packageName)
12 | s"\n${apiDefinition.nameLowerCased()}.${apiDefinition.fileEnding}"
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/app/de/innfactory/bootstrapplay2/application/keycloak/domain/models/KeycloakUserCreation.scala:
--------------------------------------------------------------------------------
1 | package de.innfactory.bootstrapplay2.application.keycloak.domain.models
2 |
3 | import play.api.libs.json.{Json, OFormat}
4 |
5 | case class KeycloakUserCreation(
6 | username: String,
7 | enabled: Boolean = true,
8 | totp: Boolean = false,
9 | emailVerified: Boolean = false,
10 | firstName: String,
11 | lastName: String,
12 | email: String
13 | )
14 |
15 | object KeycloakUserCreation {
16 | implicit val formatKeycloakUserCreation: OFormat[KeycloakUserCreation] = Json.format[KeycloakUserCreation]
17 | }
18 |
--------------------------------------------------------------------------------
/app/de/innfactory/bootstrapplay2/users/domain/models/User.scala:
--------------------------------------------------------------------------------
1 | package de.innfactory.bootstrapplay2.users.domain.models
2 |
3 | import com.google.firebase.auth.UserRecord
4 | import play.api.Logger
5 | import play.api.libs.json.{JsValue, Json}
6 |
7 | case class User(
8 | userId: UserId,
9 | email: String,
10 | emailVerified: Boolean,
11 | disabled: Boolean,
12 | claims: Claims,
13 | firstName: Option[String],
14 | lastName: Option[String],
15 | displayName: Option[String],
16 | lastSignIn: Option[Long],
17 | lastRefresh: Option[Long],
18 | creation: Option[Long]
19 | )
20 |
21 | object User {}
22 |
--------------------------------------------------------------------------------
/modules/api-definition/project/GithubConfig.scala:
--------------------------------------------------------------------------------
1 | import sbt.Credentials
2 | import sbt.Keys.credentials
3 | import sbtghpackages.GitHubPackagesPlugin.autoImport.{githubOwner, githubRepository}
4 |
5 | object GithubConfig {
6 | private val token = sys.env.getOrElse("GITHUB_TOKEN", "")
7 |
8 | val settings = Seq(
9 | githubOwner := "innFactory",
10 | githubRepository := "bootstrap-play2",
11 | credentials :=
12 | Seq(
13 | Credentials(
14 | "GitHub Package Registry",
15 | "maven.pkg.github.com",
16 | "innFactory",
17 | token
18 | )
19 | )
20 | )
21 | }
22 |
--------------------------------------------------------------------------------
/app/de/innfactory/bootstrapplay2/commons/jackson/JsValueDeSerializerModule.scala:
--------------------------------------------------------------------------------
1 | package de.innfactory.bootstrapplay2.commons.jackson
2 |
3 | import com.fasterxml.jackson.core.Version
4 | import com.fasterxml.jackson.databind.module.SimpleModule
5 | import de.innfactory.bootstrapplay2.commons.jackson.playjson.{JsValueDeserializer, JsValueSerializer}
6 | import play.api.libs.json.JsValue
7 |
8 | class JsValueDeSerializerModule() extends SimpleModule("JsValueDeSerializerModule", Version.unknownVersion()) {
9 |
10 | // first deserializers
11 | addDeserializer(classOf[JsValue], new JsValueDeserializer)
12 | addSerializer(classOf[JsValue], new JsValueSerializer)
13 |
14 | }
15 |
--------------------------------------------------------------------------------
/app/de/innfactory/bootstrapplay2/application/keycloak/domain/models/KeycloakUser.scala:
--------------------------------------------------------------------------------
1 | package de.innfactory.bootstrapplay2.application.keycloak.domain.models
2 |
3 | import play.api.libs.json.{Json, OFormat}
4 |
5 | case class KeycloakUser(
6 | id: String,
7 | createdTimestamp: Long,
8 | username: String,
9 | enabled: Boolean,
10 | totp: Boolean,
11 | emailVerified: Boolean,
12 | firstName: Option[String],
13 | lastName: Option[String],
14 | email: Option[String],
15 | requiredActions: Seq[String],
16 | notBefore: Long
17 | )
18 |
19 | object KeycloakUser {
20 | implicit val formatKeycloakUser: OFormat[KeycloakUser] = Json.format[KeycloakUser]
21 | }
22 |
--------------------------------------------------------------------------------
/app/de/innfactory/bootstrapplay2/commons/infrastructure/DatabaseHealthSocket.scala:
--------------------------------------------------------------------------------
1 | package de.innfactory.bootstrapplay2.commons.infrastructure
2 |
3 | import play.api.inject.ApplicationLifecycle
4 | import slick.jdbc.JdbcBackend.Database
5 |
6 | import java.sql.Connection
7 | import javax.inject.{Inject, Singleton}
8 | import scala.concurrent.Future
9 |
10 | @Singleton
11 | class DatabaseHealthSocket @Inject() (db: Database, lifecycle: ApplicationLifecycle) {
12 | private val connection: Connection = db.source.createConnection()
13 |
14 | def isConnectionOpen: Boolean = connection.getSchema.nonEmpty
15 |
16 | lifecycle.addStopHook { () =>
17 | Future.successful(connection.close())
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/.bin/runForDoc.md:
--------------------------------------------------------------------------------
1 | # RunFor.sh Documentation
2 |
3 | The runFor.sh Script will start a Docker Postgis Container with [docker-compose.yml](docker-compose.yml)
4 |
5 | The Postgis data volume will be mounted to:
6 |
7 | - __.bin/postgis-volume__
8 |
9 | so that even if the container is deleted no data will be lost!
10 |
11 | ### Start
12 |
13 | The Script will check for prerequisites (firebase.json) and exit if not found:
14 |
15 | ```
16 | ./local-runner/runFor.sh
17 | ```
18 |
19 | - Name mentioned in logs:
20 |
21 | ```
22 | ./local-runner/runFor.sh -n Name
23 | ```
24 |
25 | - Remove docker container volume mounted at __./local-runner/postigs__:
26 |
27 | ```
28 | ./local-runner/runFor.sh -r
29 | ```
30 |
31 |
--------------------------------------------------------------------------------
/test/testutils/grapqhl/FakeGraphQLRequest.scala:
--------------------------------------------------------------------------------
1 | package testutils.grapqhl
2 |
3 | import play.api.Application
4 | import play.api.libs.json.JsObject
5 | import play.api.mvc.{Headers, Result}
6 | import play.api.test.FakeRequest
7 | import play.api.test.Helpers.{route, POST}
8 |
9 | import scala.concurrent.Future
10 |
11 | object FakeGraphQLRequest {
12 | def getFake(body: JsObject, headers: (String, String)*)(implicit app: Application): FakeRequest[JsObject] =
13 | FakeRequest(POST, "/graphql")
14 | .withBody(body)
15 | .withHeaders(new Headers(headers))
16 |
17 | def routeResult(fakeRequest: FakeRequest[JsObject])(implicit app: Application): Future[Result] =
18 | route(app, fakeRequest).get
19 | }
20 |
--------------------------------------------------------------------------------
/doc/ModuleDocs.md:
--------------------------------------------------------------------------------
1 | # Module Docs
2 | ###### Last Updated: 17.06.2020
3 |
4 | ## Main Modules
5 |
6 |
7 | #### PlayService:
8 |
9 | - located 'app/*'
10 |
11 |
12 | ##### Submodules:
13 |
14 | - Main Application Service (de.innfactory.bootstrapplay2.common, controllers, models, Module.scala)
15 |
16 | - Data Controllers with Database Connection
17 | - DAOs
18 |
19 | - Actorsystem:
20 |
21 | - Akka System binding example for play
22 |
23 | - Websockets
24 |
25 | - Demo Websocket Controller
26 |
27 | ___
28 |
29 | #### Flyway
30 |
31 | - located 'flyway/*'
32 | - Migrates the database
33 |
34 | ___
35 |
36 | #### Slick
37 |
38 | - located 'slick/*'
39 | - Codegen for slick JDBC Database DAO
40 |
41 |
--------------------------------------------------------------------------------
/app/de/innfactory/bootstrapplay2/companies/domain/models/Company.scala:
--------------------------------------------------------------------------------
1 | package de.innfactory.bootstrapplay2.companies.domain.models
2 |
3 | import org.joda.time.DateTime
4 | import play.api.libs.json._
5 | import play.api.libs.json.JodaWrites._
6 | import play.api.libs.json.JodaReads._
7 |
8 | case class Company(
9 | id: CompanyId,
10 | settings: Option[JsValue],
11 | stringAttribute1: Option[String],
12 | stringAttribute2: Option[String],
13 | longAttribute1: Option[Long],
14 | booleanAttribute: Option[Boolean],
15 | created: DateTime,
16 | updated: DateTime
17 | ) {
18 | def patch(newObject: Company): Company =
19 | newObject.copy(
20 | id = this.id,
21 | created = this.created,
22 | updated = DateTime.now
23 | )
24 | }
25 |
--------------------------------------------------------------------------------
/app/de/innfactory/bootstrapplay2/commons/jackson/playjson/JsValueSerializer.scala:
--------------------------------------------------------------------------------
1 | package de.innfactory.bootstrapplay2.commons.jackson.playjson
2 |
3 | import com.fasterxml.jackson.core.JsonGenerator
4 | import com.fasterxml.jackson.databind.SerializerProvider
5 | import com.fasterxml.jackson.databind.ser.std.StdSerializer
6 | import play.api.Logger
7 | import play.api.libs.json.{JsValue, Json}
8 |
9 | class JsValueSerializer extends StdSerializer[JsValue](classOf[JsValue]) {
10 |
11 | private val logger = Logger("play").logger
12 |
13 | override def serialize(value: JsValue, gen: JsonGenerator, provider: SerializerProvider): Unit = {
14 | logger.trace("[JsValueSerializer] serialize " + value.toString())
15 | gen.writeString(Json.prettyPrint(value))
16 | }
17 |
18 | }
19 |
--------------------------------------------------------------------------------
/.bin/swagger/merge.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | set -e
4 |
5 | rm -f openapi-merge.json
6 |
7 | scala_folder=$(find ./modules/api/target -type d | grep -E "/scala-[0-9\.]+/resource_managed/main$")
8 | json_files=$(find "$scala_folder/" -type f -name "*.json")
9 |
10 | for file in $json_files; do
11 | title=$(cat $file | jq -r '.info.title')
12 | echo "Merging $title"
13 | cat $file | jq --arg title "$title" -r '.paths[][] |= . + { tags: [$title] }' $file >$file.tmp.json
14 | rm $file
15 | mv $file.tmp.json $file
16 | done
17 |
18 | jsons=$(find "$scala_folder" -type f -name "*.json" -exec echo '{ "inputFile": "{}" }' \; | paste -d , -s -)
19 | jq ".inputs += [$jsons]" .bin/swagger/openapi-merge.json >>openapi-merge.json
20 | npx openapi-merge-cli --config openapi-merge.json
21 |
--------------------------------------------------------------------------------
/app/de/innfactory/bootstrapplay2/graphql/ErrorParserImpl.scala:
--------------------------------------------------------------------------------
1 | package de.innfactory.bootstrapplay2.graphql
2 |
3 | import de.innfactory.play.controller.ResultStatus
4 | import de.innfactory.play.results.errors.Errors.{BadRequest, Forbidden}
5 | import de.innfactory.grapqhl.play.result.implicits.GraphQlResult.{BadRequestError, ForbiddenError}
6 | import de.innfactory.grapqhl.play.result.implicits.{ErrorParser, GraphQlException}
7 |
8 | class ErrorParserImpl extends ErrorParser[ResultStatus] {
9 | override def internalErrorToUserFacingError(error: ResultStatus): GraphQlException = error match {
10 | case _: BadRequest => BadRequestError("BadRequest")
11 | case _: Forbidden => ForbiddenError("Forbidden")
12 | case _ => BadRequestError("BadRequest")
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/.bin/setup/src/main/scala/packagedomain/scalafiles/DomainModelId.scala:
--------------------------------------------------------------------------------
1 | package packagedomain.scalafiles
2 |
3 | import config.SetupConfig
4 |
5 | case class DomainModelId(packageDomain: String, packageName: String) extends ScalaDomainFile {
6 | override def subPath =
7 | s"/$packageName/domain/models/"
8 | val name = s"${packageDomain.capitalize}Id"
9 | override def getContent(withCrud: Boolean)(implicit config: SetupConfig): String =
10 | s"""
11 | |package ${config.project.getNamespace()}.$packageName.domain.models
12 | |
13 | |import java.util.UUID
14 | |
15 | |case class $name(value: String)
16 | |
17 | |object $name {
18 | | def create: $name = $name(UUID.randomUUID().toString)
19 | |}
20 | |""".stripMargin
21 | }
22 |
--------------------------------------------------------------------------------
/app/de/innfactory/bootstrapplay2/users/infrastructure/mappers/ClaimMapper.scala:
--------------------------------------------------------------------------------
1 | package de.innfactory.bootstrapplay2.users.infrastructure.mappers
2 |
3 | import com.google.firebase.auth.UserRecord
4 | import de.innfactory.bootstrapplay2.users.domain.models.{Claims, User, UserId}
5 | import play.api.Logger
6 | import play.api.libs.json.{JsValue, Json}
7 |
8 | import scala.collection.JavaConverters._
9 |
10 | object ClaimMapper {
11 | def claimsToMap(claims: Claims): Map[String, JsValue] = {
12 | val claimSeq: Seq[(String, JsValue)] = Seq(
13 | ("innFactoryAdmin", claims.innFactoryAdmin.map(b => Json.toJson(b))),
14 | ("companyAdmin", claims.companyAdmin.map(l => Json.toJson(l)))
15 | ).filter(_._2.isDefined).map(t => (t._1, t._2.get))
16 | Map.from(claimSeq)
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/app/de/innfactory/bootstrapplay2/graphql/GraphQLController.scala:
--------------------------------------------------------------------------------
1 | package de.innfactory.bootstrapplay2.graphql
2 |
3 | import de.innfactory.bootstrapplay2.graphql.schema.SchemaDefinition
4 | import de.innfactory.grapqhl.play.controller.GraphQLControllerBase
5 | import javax.inject.{Inject, Singleton}
6 | import play.api.mvc._
7 |
8 | import scala.concurrent.ExecutionContext
9 |
10 | @Singleton
11 | class GraphQLController @Inject() (
12 | cc: ControllerComponents,
13 | executionServices: ExecutionServices,
14 | requestExecutor: RequestExecutor
15 | )(implicit ec: ExecutionContext)
16 | extends GraphQLControllerBase(cc)(
17 | executionServices,
18 | SchemaDefinition.graphQLSchema,
19 | (request: Request[AnyContent]) => Right(true),
20 | requestExecutor
21 | ) {}
22 |
--------------------------------------------------------------------------------
/.bin/setup/src/main/scala/packagedomain/smithyfiles/SmithyDomainFile.scala:
--------------------------------------------------------------------------------
1 | package packagedomain.smithyfiles
2 |
3 | import config.SetupConfig
4 | import packagedomain.DomainFile
5 |
6 | trait SmithyDomainFile extends DomainFile {
7 | val fileEnding = "smithy"
8 | val responseName: String = s"${packageDomain.capitalize}Response"
9 | val responsesName: String = s"${packageName.capitalize}Response"
10 | val requestBodyName: String = s"${packageDomain.capitalize}RequestBody"
11 |
12 | override protected def getFileName(): String =
13 | s"${nameLowerCased()}${if (fileEnding.nonEmpty) s".$fileEnding" else ""}"
14 |
15 | override protected def getFullDirectoryPath()(implicit config: SetupConfig): String =
16 | s"${System.getProperty("user.dir")}/${config.smithy.getPath}${getAdjustedSubPath()}"
17 |
18 | }
19 |
--------------------------------------------------------------------------------
/.bin/test-local.bat:
--------------------------------------------------------------------------------
1 | @echo off
2 |
3 | echo Start docker container with postgres db
4 | docker run -d -p 5432:5432 -e POSTGRES_PASSWORD=test -e POSTGRES_DB=test -e POSTGRES_USER=test --name bootstrapPlay2PGTest postgis/postgis:12-master
5 |
6 | echo Wait until container is started
7 | set /A counter=0
8 | :loop
9 | IF %counter% gtr 9 (
10 | goto failed
11 | ) ELSE (
12 | ncat -z localhost 5432 && echo Success: connection established && goto connection_established
13 | timeout /t 1 /nobreak
14 | set /A counter=%counter% + 1
15 | goto loop
16 | )
17 |
18 | :failed
19 | echo Failed establishing connection && exit 1
20 | goto:eof
21 |
22 | :connection_established
23 | echo Running tests
24 | call sbt ciTests
25 | echo Shutdown docker container
26 | docker stop bootstrapPlay2PGTest
27 | docker rm bootstrapPlay2PGTest
28 |
--------------------------------------------------------------------------------
/app/de/innfactory/bootstrapplay2/actorsystem/domain/commands/Commands.scala:
--------------------------------------------------------------------------------
1 | package de.innfactory.bootstrapplay2.actorsystem.domain.commands
2 |
3 | import akka.actor.typed.ActorRef
4 |
5 | // ############# GENERAL ###############
6 |
7 | sealed trait Command
8 | sealed trait Response
9 |
10 | final case class QueryError(query: String, replyTo: ActorRef[Response]) extends Command
11 |
12 | // ############# HELLO WORLD ###############
13 |
14 | final case class QueryHelloWorld(query: String, replyTo: ActorRef[Response]) extends Command
15 |
16 | case class ResponseQueryHelloWorld(query: String, answer: String) extends Response
17 | case class ResponseQueryHelloWorldError(query: String, error: String) extends Response
18 |
19 | case class QueryHelloWorldResult(response: Response, replyTo: ActorRef[Response]) extends Command
20 |
--------------------------------------------------------------------------------
/app/de/innfactory/bootstrapplay2/companies/infrastructure/mapper/CompanyMapper.scala:
--------------------------------------------------------------------------------
1 | package de.innfactory.bootstrapplay2.companies.infrastructure.mapper
2 |
3 | import dbdata.Tables
4 | import de.innfactory.bootstrapplay2.companies.domain.models.{Company, CompanyId}
5 | import io.scalaland.chimney.dsl.TransformerOps
6 | import org.joda.time.DateTime
7 |
8 | private[infrastructure] object CompanyMapper {
9 |
10 | implicit def companyRowToCompany(row: Tables.CompanyRow): Company =
11 | row
12 | .into[Company]
13 | .withFieldComputed(_.id, r => CompanyId(r.id))
14 | .transform
15 |
16 | implicit def companyToCompanyRow(company: Company): Tables.CompanyRow =
17 | company
18 | .into[Tables.CompanyRow]
19 | .withFieldComputed[String, String](_.id, c => c.id.value)
20 | .transform
21 |
22 | }
23 |
--------------------------------------------------------------------------------
/.bin/setup/build.sbt:
--------------------------------------------------------------------------------
1 | import sbtassembly.MergeStrategy
2 |
3 | resolvers += "Typesafe repository" at "https://repo.typesafe.com/typesafe/releases/"
4 |
5 | lazy val root = (project in file("."))
6 | .settings(
7 | name := "Setup",
8 | version := "0.0.1",
9 | scalaVersion := "2.13.13",
10 | libraryDependencies ++= Seq(
11 | "org.typelevel" %% "cats-core" % "2.8.0",
12 | "org.playframework" %% "play-json" % "2.9.2",
13 | "org.rogach" %% "scallop" % "4.1.0"
14 | ),
15 | assembly / assemblyMergeStrategy := {
16 | case "module-info.class" => MergeStrategy.discard
17 | case x =>
18 | // For all the other files, use the default sbt-assembly merge strategy
19 | val oldStrategy = (assembly / assemblyMergeStrategy).value
20 | oldStrategy(x)
21 | }
22 | )
23 |
--------------------------------------------------------------------------------
/app/de/innfactory/bootstrapplay2/commons/firebase/FirebaseBase.scala:
--------------------------------------------------------------------------------
1 | package de.innfactory.bootstrapplay2.commons.firebase
2 |
3 | import com.google.auth.oauth2.GoogleCredentials
4 | import com.google.firebase.{FirebaseApp, FirebaseOptions}
5 |
6 | object FirebaseBase {
7 |
8 | def instantiateFirebase(serviceAccountJsonFilepath: String, projectId: String = null): FirebaseApp = {
9 | val serviceAccount = getClass.getClassLoader.getResourceAsStream(serviceAccountJsonFilepath)
10 |
11 | val options: FirebaseOptions = FirebaseOptions
12 | .builder()
13 | .setCredentials(GoogleCredentials.fromStream(serviceAccount))
14 | .setProjectId(projectId)
15 | .build
16 |
17 | FirebaseApp.initializeApp(options)
18 | }
19 |
20 | def deleteFirebase(): Unit =
21 | FirebaseApp.getInstance().delete()
22 |
23 | }
24 |
--------------------------------------------------------------------------------
/test/testutils/BaseFakeRequest.scala:
--------------------------------------------------------------------------------
1 | package testutils
2 |
3 | import akka.util.Timeout
4 | import org.scalatest
5 | import play.api.Application
6 | import play.api.libs.json.{JsValue, Reads}
7 | import play.api.mvc.{AnyContentAsJson, Result}
8 | import play.api.test.FakeRequest
9 | import play.api.test.Helpers._
10 | import org.scalatest.MustMatchers._
11 |
12 | import scala.concurrent.Future
13 |
14 | object BaseFakeRequest {
15 |
16 | implicit class EnhancedResult(result: Future[Result]) {
17 | def parseContent[T](implicit reads: Reads[T]): T = {
18 | val content = contentAsJson(result)
19 | content.as[T]
20 | }
21 |
22 | def getStatus(implicit timeout: Timeout): Int =
23 | status(result)(timeout)
24 |
25 | def checkStatus(expectedStatus: Int): scalatest.Assertion =
26 | result.getStatus mustEqual expectedStatus
27 | }
28 |
29 | }
30 |
--------------------------------------------------------------------------------
/app/de/innfactory/bootstrapplay2/users/infrastructure/mappers/UserPasswordResetTokenMapper.scala:
--------------------------------------------------------------------------------
1 | package de.innfactory.bootstrapplay2.users.infrastructure.mappers
2 |
3 | import dbdata.Tables
4 | import de.innfactory.bootstrapplay2.users.domain.models.{UserId, UserPasswordResetToken}
5 | import io.scalaland.chimney.dsl.TransformerOps
6 |
7 | object UserPasswordResetTokenMapper {
8 |
9 | implicit def entityToUserPasswordResetTokensRow(
10 | entity: UserPasswordResetToken
11 | ): Tables.UserPasswordResetTokensRow =
12 | entity.into[Tables.UserPasswordResetTokensRow].withFieldComputed(_.userId, u => u.userId.value).transform
13 |
14 | implicit def rowToUserPasswordResetTokensObject(
15 | row: Tables.UserPasswordResetTokensRow
16 | ): UserPasswordResetToken =
17 | row.into[UserPasswordResetToken].withFieldComputed(_.userId, u => UserId(u.userId)).transform
18 | }
19 |
--------------------------------------------------------------------------------
/app/de/innfactory/bootstrapplay2/commons/results/ErrorResponseWithAdditionalBody.scala:
--------------------------------------------------------------------------------
1 | package de.innfactory.bootstrapplay2.commons.results
2 |
3 | import de.innfactory.play.controller.ErrorResponse
4 | import play.api.libs.json.{JsValue, Json}
5 | import play.api.mvc.{AnyContent, Request}
6 |
7 | case class ErrorResponseWithAdditionalBody(message: String, details: JsValue) {
8 | def toJson = Json.toJson(this)(ErrorResponseWithAdditionalBody.writes)
9 | }
10 |
11 | object ErrorResponseWithAdditionalBody {
12 |
13 | implicit val reads = Json.reads[ErrorResponseWithAdditionalBody]
14 | implicit val writes = Json.writes[ErrorResponseWithAdditionalBody]
15 |
16 | def fromRequest(message: String)(implicit request: Request[AnyContent]) =
17 | Json.toJson(ErrorResponse(message))
18 |
19 | def fromMessage(message: String) =
20 | Json.toJson(ErrorResponse(message))
21 |
22 | }
23 |
--------------------------------------------------------------------------------
/app/de/innfactory/bootstrapplay2/websockets/domain/DomainWebSocketService.scala:
--------------------------------------------------------------------------------
1 | package de.innfactory.bootstrapplay2.websockets.domain
2 |
3 | import akka.actor.ActorSystem
4 | import akka.stream.Materializer
5 | import akka.stream.scaladsl.Flow
6 | import de.innfactory.bootstrapplay2.actorsystem.domain.commands.Command
7 | import de.innfactory.bootstrapplay2.websockets.domain.interfaces.{WebSocketRepository, WebSocketService}
8 | import play.api.libs.streams.ActorFlow
9 | import play.api.mvc.WebSocket
10 |
11 | import javax.inject.Inject
12 |
13 | class DomainWebSocketService @Inject() (webSocketRepository: WebSocketRepository)(implicit
14 | val system: ActorSystem,
15 | mat: Materializer
16 | ) extends WebSocketService {
17 | override def socket: Flow[Any, Nothing, _] =
18 | ActorFlow.actorRef { out =>
19 | webSocketRepository.createWebSocketActor(out)
20 | }
21 |
22 | }
23 |
--------------------------------------------------------------------------------
/app/de/innfactory/bootstrapplay2/commons/jackson/playjson/JsValueDeserializer.scala:
--------------------------------------------------------------------------------
1 | package de.innfactory.bootstrapplay2.commons.jackson.playjson
2 |
3 | import com.fasterxml.jackson.core.{JsonParser, JsonTokenId}
4 | import com.fasterxml.jackson.databind.DeserializationContext
5 | import com.fasterxml.jackson.databind.deser.std.StdDeserializer
6 | import play.api.Logger
7 | import play.api.libs.json.{JsNull, JsValue, Json}
8 |
9 | class JsValueDeserializer extends StdDeserializer[JsValue](classOf[JsValue]) {
10 |
11 | private val logger = Logger("play").logger
12 |
13 | override def deserialize(p: JsonParser, ctxt: DeserializationContext): JsValue = {
14 | logger.trace("[JsValueDeserializer] deserialize " + p.getText)
15 |
16 | p.currentTokenId() match {
17 | case JsonTokenId.ID_STRING => Json.parse(p.getText)
18 | case _ => JsNull
19 | }
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/app/de/innfactory/bootstrapplay2/filters/MiddlewareRegistry.scala:
--------------------------------------------------------------------------------
1 | package de.innfactory.bootstrapplay2.filters
2 |
3 | import de.innfactory.bootstrapplay2.filters.access.RouteBlacklistFilter
4 | import de.innfactory.bootstrapplay2.filters.logging.AccessLoggingFilter
5 | import de.innfactory.bootstrapplay2.filters.tracing.TracingFilter
6 | import de.innfactory.smithy4play.middleware.{MiddlewareBase, MiddlewareRegistryBase, ValidateAuthMiddleware}
7 |
8 | import javax.inject.Inject
9 |
10 | class MiddlewareRegistry @Inject() (
11 | routeBlacklistFilter: RouteBlacklistFilter,
12 | accessLoggingFilter: AccessLoggingFilter,
13 | tracingFilter: TracingFilter,
14 | validateAuthMiddleware: ValidateAuthMiddleware
15 | ) extends MiddlewareRegistryBase {
16 | override val middlewares: Seq[MiddlewareBase] =
17 | Seq(tracingFilter, routeBlacklistFilter, accessLoggingFilter, validateAuthMiddleware)
18 | }
19 |
--------------------------------------------------------------------------------
/modules/slick/src/main/scala/db/codegen/CustomizedCodeGenerator.scala:
--------------------------------------------------------------------------------
1 | package db.codegen
2 | import de.innfactory.play.db.codegen.XPostgresProfile
3 | import de.innfactory.play.db.codegen.{Config, CustomizedCodeGeneratorBase, CustomizedCodeGeneratorConfig}
4 |
5 | class CodeGenConfig() extends Config[XPostgresProfile] {
6 | override lazy val slickProfile: XPostgresProfile = XPostgresProfile
7 | }
8 |
9 | object CustomizedCodeGenerator
10 | extends CustomizedCodeGeneratorBase(
11 | CustomizedCodeGeneratorConfig(
12 | folder = "/target/scala-2.13/src_managed/main"
13 | ),
14 | new CodeGenConfig()
15 | ) {
16 |
17 | // Update here if new Tables are added
18 | // Each Database Table, which should be included in CodeGen
19 | // has to be added here in UPPER-CASE
20 | override def included: Seq[String] = Seq("company", "location", "user_password_reset_tokens").map(_.toUpperCase)
21 | }
22 |
--------------------------------------------------------------------------------
/app/de/innfactory/bootstrapplay2/websockets/infrastructure/actors/ScheduledActor.scala:
--------------------------------------------------------------------------------
1 | package de.innfactory.bootstrapplay2.websockets.infrastructure.actors
2 |
3 | import akka.Done
4 | import akka.actor._
5 | import play.api.libs.json.JsValue
6 |
7 | object ScheduledActor {
8 | def props(out: ActorRef, messageCount: Int, delay: Long): Props = Props(new ScheduledActor(out, messageCount, delay))
9 | }
10 | class ScheduledActor(out: ActorRef, messageCount: Int, delay: Long) extends Actor {
11 | var counter = 1
12 |
13 | def send(): Unit = {
14 | Thread.sleep(delay)
15 | out ! s"Test Message ${counter}"
16 | println(s"Sending $counter")
17 | counter = counter + 1
18 | if (counter < messageCount) {
19 | send()
20 | } else {
21 | out ! Done
22 | }
23 | }
24 |
25 | def receive: PartialFunction[Any, Unit] = { case _ =>
26 | println("Receive unknown")
27 | }
28 |
29 | send()
30 | }
31 |
--------------------------------------------------------------------------------
/app/de/innfactory/bootstrapplay2/commons/jwt/AWSJWTValidator.scala:
--------------------------------------------------------------------------------
1 | package de.innfactory.bootstrapplay2.commons.jwt
2 | import java.net.URL
3 | import com.nimbusds.jose.jwk.source.{JWKSource, RemoteJWKSet}
4 | import com.nimbusds.jose.proc.SecurityContext
5 | import com.nimbusds.jose.util.DefaultResourceRetriever
6 | import de.innfactory.bootstrapplay2.commons.jwt.AWSJWTValidator.DEFAULT_HTTP_SIZE_LIMIT
7 |
8 | object AWSJWTValidator {
9 | val DEFAULT_HTTP_SIZE_LIMIT: Int = 25 * 1024 * 1024
10 | def apply(
11 | url: JWKUrl
12 | ): AWSJWTValidator = new AWSJWTValidator(url)
13 | }
14 |
15 | final class AWSJWTValidator(url: JWKUrl) extends JWTValidatorBase {
16 |
17 | val issuer: String = url.value
18 |
19 | val jwkSetSource: JWKSource[SecurityContext] = new RemoteJWKSet(
20 | new URL(s"${url.value}/.well-known/jwks.json"),
21 | new DefaultResourceRetriever(4000, 4000, DEFAULT_HTTP_SIZE_LIMIT)
22 | )
23 |
24 | }
25 |
--------------------------------------------------------------------------------
/app/de/innfactory/bootstrapplay2/websockets/infrastructure/actors/WebSocketActor.scala:
--------------------------------------------------------------------------------
1 | package de.innfactory.bootstrapplay2.websockets.infrastructure.actors
2 |
3 | import akka.actor._
4 | import play.api.libs.json.JsValue
5 |
6 | object WebSocketActor {
7 | def props(out: ActorRef): Props = Props(new WebSocketActor(out))
8 | }
9 |
10 | case class Test(name: String, message: String)
11 |
12 | import play.api.libs.json.Json
13 | object Test {
14 | implicit val format = Json.format[Test]
15 | }
16 |
17 | class WebSocketActor(out: ActorRef) extends Actor {
18 | var counter = 1
19 | def receive: PartialFunction[Any, Unit] = {
20 | case jsValue: JsValue if jsValue.validate[Test].asOpt.isDefined =>
21 | out ! Json.toJson(
22 | Test(this.self.path.toString, "I received your message: " + jsValue + " | This is message: " + counter)
23 | )
24 | counter += 1
25 | case _ => println("Receive unknown")
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/modules/api-definition/src/main/resources/META-INF/smithy/actorSystemAPIController.smithy:
--------------------------------------------------------------------------------
1 | $version: "2.0"
2 |
3 | namespace de.innfactory.bootstrapplay2.api
4 | use alloy#simpleRestJson
5 |
6 | @simpleRestJson
7 | service ActorSystemAPIController {
8 | version: "1.0.0",
9 | operations: [
10 | helloworldViaSystem
11 | ]
12 | }
13 |
14 | // --------- OPERATIONS -------------
15 |
16 | @http(method: "GET", uri: "/v1/public/helloworld/system/{query}", code: 200)
17 | @readonly
18 | operation helloworldViaSystem {
19 | input: HelloworldViaSystemRequest,
20 | output: HelloworldViaSystemResponse
21 | }
22 |
23 | // --------- REQUESTS -------------
24 |
25 | structure HelloworldViaSystemRequest {
26 | @httpLabel
27 | @required
28 | query: String
29 | }
30 |
31 | // --------- RESPONSES -------------
32 |
33 | structure HelloworldViaSystemResponse {
34 | @httpLabel
35 | @required
36 | answer: String
37 | }
--------------------------------------------------------------------------------
/app/de/innfactory/bootstrapplay2/companies/domain/models/CompanyId.scala:
--------------------------------------------------------------------------------
1 | package de.innfactory.bootstrapplay2.companies.domain.models
2 |
3 | import de.innfactory.bootstrapplay2.api
4 | import io.scalaland.chimney.Transformer
5 |
6 | import java.util.UUID
7 |
8 | case class CompanyId(value: String)
9 |
10 | object CompanyId {
11 | def create: CompanyId = CompanyId(UUID.randomUUID().toString)
12 |
13 | implicit val companyIdFromDomain = (companyId: CompanyId) =>
14 | de.innfactory.bootstrapplay2.api.CompanyId(companyId.value)
15 |
16 | implicit val companyIdToDomain = (id: de.innfactory.bootstrapplay2.api.CompanyId) => CompanyId(id.value)
17 |
18 | implicit val companyIdFromDomainTransformer: Transformer[CompanyId, de.innfactory.bootstrapplay2.api.CompanyId] =
19 | companyIdFromDomain(_)
20 | implicit val companyIdToDomainTransformer: Transformer[de.innfactory.bootstrapplay2.api.CompanyId, CompanyId] =
21 | companyIdToDomain(_)
22 | }
23 |
--------------------------------------------------------------------------------
/modules/api-definition/src/main/resources/META-INF/smithy/actorShardingAPIController.smithy:
--------------------------------------------------------------------------------
1 | $version: "2.0"
2 |
3 | namespace de.innfactory.bootstrapplay2.api
4 | use alloy#simpleRestJson
5 |
6 | @simpleRestJson
7 | service ActorShardingAPIController {
8 | version: "1.0.0",
9 | operations: [
10 | helloworldViaSharding
11 | ]
12 | }
13 |
14 | // --------- OPERATIONS -------------
15 |
16 | @http(method: "GET", uri: "/v1/public/helloworld/sharding/{query}", code: 200)
17 | @readonly
18 | operation helloworldViaSharding {
19 | input: HelloworldViaShardingRequest,
20 | output: HelloworldViaShardingResponse
21 | }
22 |
23 | // --------- REQUESTS -------------
24 |
25 | structure HelloworldViaShardingRequest {
26 | @httpLabel
27 | @required
28 | query: String
29 | }
30 |
31 | // --------- RESPONSES -------------
32 |
33 | structure HelloworldViaShardingResponse {
34 | @httpLabel
35 | @required
36 | answer: String
37 | }
--------------------------------------------------------------------------------
/app/de/innfactory/bootstrapplay2/commons/jwt/JWTValidatorBase.scala:
--------------------------------------------------------------------------------
1 | package de.innfactory.bootstrapplay2.commons.jwt
2 |
3 | import com.nimbusds.jose.jwk.source.JWKSource
4 | import com.nimbusds.jose.proc.SecurityContext
5 | import com.nimbusds.jwt.JWTClaimsSet
6 | import com.nimbusds.jwt.proc.BadJWTException
7 | import de.innfactory.bootstrapplay2.commons.jwt.algorithm.JWTAlgorithm
8 | import de.innfactory.play.smithy4play.JWTToken
9 |
10 | abstract class JWTValidatorBase extends JWTValidator {
11 |
12 | val issuer: String
13 |
14 | val jwkSetSource: JWKSource[SecurityContext]
15 |
16 | private lazy val configurableJwtValidator =
17 | new ConfigurableJWTValidator(
18 | keySource = jwkSetSource,
19 | additionalValidations = List(),
20 | algorithm = JWTAlgorithm.RS512
21 | )
22 |
23 | override def validate(jwtToken: JWTToken): Either[BadJWTException, (JWTToken, JWTClaimsSet)] =
24 | configurableJwtValidator.validate(jwtToken)
25 |
26 | }
27 |
--------------------------------------------------------------------------------
/app/de/innfactory/bootstrapplay2/locations/domain/models/LocationId.scala:
--------------------------------------------------------------------------------
1 | package de.innfactory.bootstrapplay2.locations.domain.models
2 |
3 | import de.innfactory.bootstrapplay2.api
4 | import io.scalaland.chimney.Transformer
5 |
6 | import java.util.UUID
7 |
8 | case class LocationId(value: String)
9 |
10 | object LocationId {
11 | def create: LocationId = LocationId(UUID.randomUUID().toString)
12 |
13 | implicit val locationIdFromDomain = (locationId: LocationId) =>
14 | de.innfactory.bootstrapplay2.api.LocationId(locationId.value)
15 |
16 | implicit val locationIdToDomain = (id: de.innfactory.bootstrapplay2.api.LocationId) => LocationId(id.value)
17 |
18 | implicit val locationIdFromDomainTransformer: Transformer[LocationId, de.innfactory.bootstrapplay2.api.LocationId] =
19 | locationIdFromDomain(_)
20 | implicit val locationIdToDomainTransformer: Transformer[de.innfactory.bootstrapplay2.api.LocationId, LocationId] =
21 | locationIdToDomain(_)
22 | }
23 |
--------------------------------------------------------------------------------
/app/de/innfactory/bootstrapplay2/graphql/schema/models/Locations.scala:
--------------------------------------------------------------------------------
1 | package de.innfactory.bootstrapplay2.graphql.schema.models
2 |
3 | import de.innfactory.bootstrapplay2.graphql.GraphQLExecutionContext
4 | import de.innfactory.bootstrapplay2.locations.domain.models.Location
5 | import sangria.macros.derive.{deriveObjectType, ReplaceField}
6 | import sangria.schema.{BigDecimalType, Field, ObjectType, OptionType, StringType}
7 | import de.innfactory.grapqhl.sangria.implicits.JsonScalarType._
8 | import de.innfactory.grapqhl.sangria.implicits.JodaScalarType._
9 |
10 | object Locations {
11 | val LocationType: ObjectType[Unit, Location] = deriveObjectType(
12 | ReplaceField(
13 | fieldName = "id",
14 | field = Field(name = "id", fieldType = StringType, resolve = c => c.value.id.value)
15 | ),
16 | ReplaceField(
17 | fieldName = "company",
18 | field = Field(name = "company", fieldType = StringType, resolve = c => c.value.company.value)
19 | )
20 | )
21 | }
22 |
--------------------------------------------------------------------------------
/test/testutils/grapqhl/CompanyRequests.scala:
--------------------------------------------------------------------------------
1 | package testutils.grapqhl
2 |
3 | import de.innfactory.bootstrapplay2.api.CompanyResponse
4 | import play.api.libs.json.{JsObject, Json}
5 |
6 | object CompanyRequests {
7 |
8 | object CompanyRequest {
9 | private val body = Json.parse("""{"operationName":"Companies"}""")
10 |
11 | def getRequest(filter: Option[String]): JsObject = {
12 | val addition = if (filter.isDefined) "( filter: \"" + filter.get + "\")" else ""
13 | body.as[JsObject] ++ Json.obj(
14 | "query" ->
15 | s"""query Companies {
16 | | allCompanies$addition {
17 | | id
18 | | settings
19 | | stringAttribute1
20 | | stringAttribute2
21 | | longAttribute1
22 | | booleanAttribute
23 | | created
24 | | updated
25 | | }
26 | |}""".stripMargin
27 | )
28 | }
29 | }
30 |
31 | }
32 |
--------------------------------------------------------------------------------
/.bin/test-local.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | RED='\033[0;31m'
4 | ORANGE='\033[0;33m'
5 | GREEN='\033[0;32m'
6 | BLUE='\033[0;34m'
7 | NC='\033[0m' # No Color
8 |
9 | REMOVE=$GITHUB_TOKEN
10 |
11 | if [ "$REMOVE" == "" ]; then
12 | printf "${RED}NO GITHUB TOKEN SET! ${NC} \n" && exit 1
13 | fi
14 |
15 |
16 |
17 |
18 | docker run -d -p 5432:5432 -e POSTGRES_PASSWORD=test -e POSTGRES_DB=test -e POSTGRES_USER=test --name bootstrapPlay2PGTest postgis/postgis:12-master
19 |
20 | FILE=build.sbt
21 | if test -f "$FILE"; then
22 | cd .bin
23 | fi
24 |
25 | (
26 | for i in `seq 1 10`;
27 | do
28 | nc -z localhost 5432 && echo Success && exit 0
29 | echo -n .
30 | sleep 1
31 | done
32 | echo Failed waiting for Postgres && exit 1
33 | )
34 |
35 |
36 | firebase emulators:exec "cd .. && export FIREBASE_AUTH_EMULATOR_HOST='localhost:9099' && sbt ciTests" --only auth --project demo-bootstrap-play2 --import=./firebase-data
37 | docker stop bootstrapPlay2PGTest
38 | docker rm bootstrapPlay2PGTest
--------------------------------------------------------------------------------
/app/de/innfactory/bootstrapplay2/users/domain/interfaces/UserPasswordResetTokenRepository.scala:
--------------------------------------------------------------------------------
1 | package de.innfactory.bootstrapplay2.users.domain.interfaces
2 |
3 | import cats.data.EitherT
4 | import com.google.inject.ImplementedBy
5 | import de.innfactory.play.tracing.TraceContext
6 | import de.innfactory.bootstrapplay2.users.domain.models.{UserId, UserPasswordResetToken}
7 | import de.innfactory.bootstrapplay2.users.infrastructure.SlickUserResetTokenRepository
8 | import de.innfactory.play.controller.ResultStatus
9 |
10 | import scala.concurrent.Future
11 |
12 | @ImplementedBy(classOf[SlickUserResetTokenRepository])
13 | trait UserPasswordResetTokenRepository {
14 | def getForUser(userId: UserId)(implicit rc: TraceContext): EitherT[Future, ResultStatus, UserPasswordResetToken]
15 |
16 | def create(entity: UserPasswordResetToken)(implicit
17 | rc: TraceContext
18 | ): EitherT[Future, ResultStatus, UserPasswordResetToken]
19 |
20 | def delete(entity: UserPasswordResetToken)(implicit rc: TraceContext): EitherT[Future, ResultStatus, Int]
21 | }
22 |
--------------------------------------------------------------------------------
/app/de/innfactory/bootstrapplay2/commons/logging/LoggingEnhancer.scala:
--------------------------------------------------------------------------------
1 | package de.innfactory.bootstrapplay2.commons.logging
2 |
3 | import io.opentelemetry.api.trace.Span
4 | import org.slf4j.{Marker, MarkerFactory}
5 | import play.api.Logger
6 |
7 | object LoggingEnhancer {
8 |
9 | private def spanToMarker(span: Span): String =
10 | "tracer=" + span.getSpanContext.getTraceId
11 |
12 | private def getMarker(span: Span): Marker =
13 | MarkerFactory.getMarker(spanToMarker(span))
14 |
15 | implicit class LoggingEnhancer(logger: Logger) {
16 | def tracedWarn(message: String)(implicit span: Span) =
17 | logger.logger.warn(getMarker(span), message)
18 |
19 | def tracedError(message: String)(implicit span: Span) =
20 | logger.logger.error(getMarker(span), message)
21 |
22 | def tracedInfo(message: String)(implicit span: Span) =
23 | logger.logger.info(getMarker(span), message)
24 |
25 | def tracedDebug(message: String)(implicit span: Span) =
26 | logger.logger.info(getMarker(span), message)
27 | }
28 |
29 | }
30 |
--------------------------------------------------------------------------------
/app/de/innfactory/bootstrapplay2/users/application/models/UserPasswordResetTokenResponse.scala:
--------------------------------------------------------------------------------
1 | package de.innfactory.bootstrapplay2.users.application.models
2 |
3 | import de.innfactory.bootstrapplay2.users.domain.models.{UserId, UserPasswordResetToken}
4 | import org.joda.time.DateTime
5 | import org.joda.time.DateTime
6 | import play.api.libs.json.{JsValue, Json}
7 | import play.api.libs.json.JodaWrites._
8 | import play.api.libs.json.JodaReads._
9 | import io.scalaland.chimney.dsl._
10 |
11 | case class UserPasswordResetTokenResponse(
12 | userId: String,
13 | token: String,
14 | created: DateTime,
15 | validUntil: DateTime
16 | )
17 |
18 | object UserPasswordResetTokenResponse {
19 | implicit val format = Json.format[UserPasswordResetTokenResponse]
20 |
21 | def fromUserPasswordResetToken(userPasswordResetToken: UserPasswordResetToken): UserPasswordResetTokenResponse =
22 | userPasswordResetToken
23 | .into[UserPasswordResetTokenResponse]
24 | .withFieldComputed(_.userId, u => u.userId.value)
25 | .transform
26 | }
27 |
--------------------------------------------------------------------------------
/.bin/setup/src/main/scala/packagedomain/scalafiles/DomainModel.scala:
--------------------------------------------------------------------------------
1 | package packagedomain.scalafiles
2 |
3 | import config.SetupConfig
4 |
5 | case class DomainModel(packageDomain: String, packageName: String) extends ScalaDomainFile {
6 | override def subPath =
7 | s"/$packageName/domain/models/"
8 | val name = s"${packageDomain.capitalize}"
9 | override def getContent(withCrud: Boolean)(implicit config: SetupConfig): String = {
10 | val domainModelId = DomainModelId(packageDomain, packageName)
11 | s"""
12 | |package ${config.project.getNamespace()}.$packageName.domain.models
13 | |
14 | |import org.joda.time.DateTime
15 | |
16 | |case class $name(
17 | | id: ${domainModelId.name},
18 | | created: DateTime,
19 | | updated: DateTime
20 | |) {
21 | | def patch(newObject: $name): $name =
22 | | newObject.copy(
23 | | id = this.id,
24 | | created = this.created,
25 | | updated = DateTime.now
26 | | )
27 | |}
28 | |""".stripMargin
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/app/de/innfactory/bootstrapplay2/graphql/schema/queries/Company.scala:
--------------------------------------------------------------------------------
1 | package de.innfactory.bootstrapplay2.graphql.schema.queries
2 |
3 | import de.innfactory.bootstrapplay2.commons.RequestContext
4 | import de.innfactory.bootstrapplay2.graphql.GraphQLExecutionContext
5 | import de.innfactory.bootstrapplay2.graphql.RequestExecutor.EnhancedRequest
6 | import de.innfactory.bootstrapplay2.graphql.schema.models.Companies.CompanyType
7 | import de.innfactory.bootstrapplay2.graphql.schema.models.Arguments.FilterArg
8 | import sangria.schema.{Field, ListType}
9 |
10 | object Company {
11 | val allCompanies: Field[GraphQLExecutionContext, Unit] = Field(
12 | "allCompanies",
13 | ListType(CompanyType),
14 | arguments = FilterArg :: Nil,
15 | resolve = ctx => {
16 | ctx.ctx.request.toRequestContextAndExecute(
17 | "allCompanies GraphQL",
18 | (rc: RequestContext) => ctx.ctx.companiesService.getAllForGraphQL(ctx arg FilterArg)(rc)
19 | )(ctx.ctx.ec)
20 | },
21 | description = Some("Bootstrap Filter API companies query. Query group by id")
22 | )
23 |
24 | }
25 |
--------------------------------------------------------------------------------
/app/de/innfactory/bootstrapplay2/locations/infrastructure/mapper/LocationMapper.scala:
--------------------------------------------------------------------------------
1 | package de.innfactory.bootstrapplay2.locations.infrastructure.mapper
2 |
3 | import dbdata.Tables
4 | import de.innfactory.bootstrapplay2.companies.domain.models.CompanyId
5 | import de.innfactory.bootstrapplay2.locations.domain.models.{Location, LocationId}
6 | import io.scalaland.chimney.dsl.TransformerOps
7 | import org.joda.time.DateTime
8 |
9 | private[infrastructure] object LocationMapper {
10 |
11 | implicit def locationRowToLocations(row: Tables.LocationRow): Location =
12 | row
13 | .into[Location]
14 | .withFieldComputed[LocationId, LocationId](_.id, r => LocationId(r.id))
15 | .withFieldComputed[CompanyId, CompanyId](_.company, r => CompanyId(r.company))
16 | .transform
17 |
18 | implicit def locationToLocationRow(location: Location): Tables.LocationRow =
19 | location
20 | .into[Tables.LocationRow]
21 | .withFieldComputed[String, String](_.id, c => c.id.value)
22 | .withFieldComputed[String, String](_.company, c => c.company.value)
23 | .transform
24 |
25 | }
26 |
--------------------------------------------------------------------------------
/app/de/innfactory/bootstrapplay2/graphql/schema/SchemaDefinition.scala:
--------------------------------------------------------------------------------
1 | package de.innfactory.bootstrapplay2.graphql.schema
2 |
3 | import de.innfactory.bootstrapplay2.graphql.GraphQLExecutionContext
4 | import de.innfactory.grapqhl.sangria.resolvers.generic.CustomRootResolver
5 | import de.innfactory.bootstrapplay2.graphql.schema.mutations.MutationDefinition.Mutations
6 | import de.innfactory.bootstrapplay2.graphql.schema.queries.QueryDefinition.Query
7 | import sangria.execution.deferred.DeferredResolver
8 | import sangria.schema.Schema
9 |
10 | /**
11 | * Defines a GraphQL schema for the current project
12 | */
13 | object SchemaDefinition {
14 |
15 | // Resolvers (CustomRootResolver with DeferredResolver) and Fetchers
16 | val resolvers: DeferredResolver[GraphQLExecutionContext] = DeferredResolver.fetchersWithFallback(
17 | new CustomRootResolver(
18 | Map()
19 | )
20 | )
21 |
22 | val graphQLSchema: Schema[GraphQLExecutionContext, Unit] =
23 | Schema(
24 | Query,
25 | None,
26 | description = Some(
27 | "Schema for Bootstrap API "
28 | )
29 | )
30 |
31 | }
32 |
--------------------------------------------------------------------------------
/app/de/innfactory/bootstrapplay2/actorsharding/domain/common/Sharding.scala:
--------------------------------------------------------------------------------
1 | package de.innfactory.bootstrapplay2.actorsharding.domain.common
2 |
3 | import akka.actor.ActorSystem
4 | import akka.actor.typed.scaladsl.adapter.ClassicActorSystemOps
5 | import akka.cluster.sharding.typed.scaladsl.ClusterSharding
6 | import akka.util.Timeout
7 |
8 | import javax.inject.{Inject, Singleton}
9 | import scala.concurrent.ExecutionContext
10 | import scala.concurrent.duration._
11 |
12 | @Singleton
13 | class Sharding @Inject() (system: ActorSystem)(implicit ec: ExecutionContext) {
14 |
15 | implicit val timeout: Timeout = 10.seconds
16 |
17 | // Convert classic actor system of play to typed
18 | private val actorSystem: akka.actor.typed.ActorSystem[_] = system.toTyped
19 |
20 | private val scheduler: akka.actor.typed.Scheduler = actorSystem.scheduler
21 |
22 | private val sharding: ClusterSharding = ClusterSharding(actorSystem)
23 |
24 | def getSharding: ClusterSharding = sharding
25 | def getActorSystem: akka.actor.typed.ActorSystem[_] = actorSystem
26 | def getScheduler: akka.actor.typed.Scheduler = scheduler
27 |
28 | }
29 |
--------------------------------------------------------------------------------
/project/Build.scala:
--------------------------------------------------------------------------------
1 | import sbt.Keys.{envVars, _}
2 | import sbt.{Resolver, _}
3 |
4 | object Common {
5 |
6 | def projectSettings =
7 | Seq(
8 | scalaVersion := Dependencies.scalaVersion, // todo fix several version statements in sbt files
9 | // javacOptions ++= Seq("-source", "11", "-target", "11"),
10 | scalacOptions ++= Seq(
11 | "-encoding",
12 | "UTF-8", // yes, this is 2 args
13 | "-deprecation",
14 | "-feature",
15 | "-unchecked",
16 | "-Xlint",
17 | "-Yno-adapted-args",
18 | "-Ywarn-numeric-widen"
19 | ),
20 | resolvers ++= Seq(
21 | "scalaz-bintray" at "http://dl.bintray.com/scalaz/releases",
22 | Resolver.sonatypeRepo("releases"),
23 | Resolver.sonatypeRepo("snapshots")
24 | ),
25 | libraryDependencies ++= Seq(
26 | "javax.inject" % "javax.inject" % "1",
27 | "joda-time" % "joda-time" % "2.9.9",
28 | "org.joda" % "joda-convert" % "1.9.2",
29 | "com.google.inject" % "guice" % "5.0.1"
30 | ),
31 | Test / scalacOptions ++= Seq("-Yrangepos")
32 | )
33 | }
34 |
--------------------------------------------------------------------------------
/conf/routes:
--------------------------------------------------------------------------------
1 | # Routes
2 | # This file defines all application routes (Higher priority routes first)
3 | # ~~~~
4 |
5 | -> / de.innfactory.smithy4play.AutoRouter
6 |
7 | POST /graphql de.innfactory.bootstrapplay2.graphql.GraphQLController.graphql
8 | POST /graphql/schema de.innfactory.bootstrapplay2.graphql.GraphQLController.renderSchema
9 |
10 | # - - - - - - - - WEBSOCKET - - - - - - - -
11 | ###
12 | # summary: Websocket
13 | # tags:
14 | # - public
15 | # responses:
16 | # '200':
17 | # description: Response
18 | ###
19 | GET /v1/websocket de.innfactory.bootstrapplay2.websockets.application.WebsocketController.socket
20 |
21 | # Map static resources from the /public folder to the /assets URL path
22 | ### NoDocs ###
23 | #GET /assets/*file controllers.Assets.versioned(path="/public", file: Asset)
24 | ### NoDocs ###
25 | #GET /v1/assets/*file controllers.Assets.versioned(path="/public", file: Asset)
--------------------------------------------------------------------------------
/app/de/innfactory/bootstrapplay2/commons/jwt/algorithm/JWTAlgorithm.scala:
--------------------------------------------------------------------------------
1 | package de.innfactory.bootstrapplay2.commons.jwt.algorithm
2 |
3 | import com.nimbusds.jose.JWSAlgorithm
4 |
5 | sealed trait JWTAlgorithm {
6 | def name: String
7 | def fullName: String
8 | def nimbusRepresentation: JWSAlgorithm
9 | }
10 |
11 | sealed trait JwtAsymmetricAlgorithm extends JWTAlgorithm {}
12 | sealed trait JwtRSAAlgorithm extends JwtAsymmetricAlgorithm {}
13 |
14 | object JWTAlgorithm {
15 |
16 | def fromString(algo: String): JWTAlgorithm = algo match {
17 | case "RS256" => RS256
18 | case "RS384" => RS384
19 | case "RS512" => RS512
20 | case _ => RS512
21 | }
22 |
23 | case object RS256 extends JwtRSAAlgorithm {
24 | def name = "RS256"; def fullName = "SHA256withRSA"; def nimbusRepresentation = JWSAlgorithm.RS256
25 | }
26 | case object RS384 extends JwtRSAAlgorithm {
27 | def name = "RS384"; def fullName = "SHA384withRSA"; def nimbusRepresentation = JWSAlgorithm.RS384
28 | }
29 | case object RS512 extends JwtRSAAlgorithm {
30 | def name = "RS512"; def fullName = "SHA512withRSA"; def nimbusRepresentation = JWSAlgorithm.RS512
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/.bin/setup/src/main/scala/arguments/stringarg/StringArgRetriever.scala:
--------------------------------------------------------------------------------
1 | package arguments.stringarg
2 |
3 | import scala.annotation.tailrec
4 | import scala.io.StdIn.readLine
5 |
6 | object StringArgRetriever {
7 | @tailrec
8 | def askFor(
9 | message: String,
10 | validations: Seq[String => Either[String, Unit]] =
11 | Seq(StringArgValidations.cantBeEmpty, StringArgValidations.onlyLetters)
12 | ): String = {
13 | println(message)
14 | val arg = readLine().toLowerCase()
15 | validateInput(validations.map(validate => validate(arg))) match {
16 | case Left(validationErrors) =>
17 | println(validationErrors)
18 | askFor(message, validations)
19 | case Right(_) => arg
20 | }
21 | }
22 |
23 | private def validateInput(validations: Seq[Either[String, Unit]]) =
24 | validations.fold(Right(())) { (result, validation) =>
25 | validation match {
26 | case Left(validationError) =>
27 | result match {
28 | case Left(resultError) => Left(s"$resultError\n$validationError")
29 | case Right(_) => Left(validationError)
30 | }
31 | case Right(_) => result
32 | }
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/.deployment/deployment.tpl.yaml:
--------------------------------------------------------------------------------
1 | ---
2 | kind: Role
3 | apiVersion: rbac.authorization.k8s.io/v1
4 | metadata:
5 | name: pod-reader
6 | namespace: "$NAMESPACE"
7 | rules:
8 | - apiGroups: [""]
9 | resources: ["pods"]
10 | verbs: ["get", "watch", "list"]
11 | ---
12 | kind: RoleBinding
13 | apiVersion: rbac.authorization.k8s.io/v1
14 | metadata:
15 | name: read-pods
16 | namespace: "$NAMESPACE"
17 | subjects:
18 | - kind: User
19 | name: "system:serviceaccount:$NAMESPACE:$SANAME"
20 | roleRef:
21 | kind: Role
22 | name: pod-reader
23 | apiGroup: rbac.authorization.k8s.io
24 |
25 | # TODO add `automountServiceAccountToken: true` to pod deployment!
26 |
27 | ports:
28 | - containerPort: 8080
29 | name: http
30 | - name: management
31 | containerPort: 8558
32 | protocol: TCP
33 | - name: remote
34 | containerPort: 25520
35 | protocol: TCP
36 | readinessProbe:
37 | httpGet:
38 | path: "/ready"
39 | port: management
40 | periodSeconds: 10
41 | failureThreshold: 10
42 | initialDelaySeconds: 20
43 | livenessProbe:
44 | httpGet:
45 | path: "/alive"
46 | port: management
47 | periodSeconds: 10
48 | failureThreshold: 10
49 | initialDelaySeconds: 60
--------------------------------------------------------------------------------
/.bin/setup/src/main/scala/arguments/stringlistarg/StringListArgRetriever.scala:
--------------------------------------------------------------------------------
1 | package arguments.stringlistarg
2 |
3 | import scala.annotation.tailrec
4 | import scala.io.StdIn.readLine
5 |
6 | object StringListArgRetriever {
7 | @tailrec
8 | def askFor(
9 | message: String,
10 | validations: Seq[String => Either[String, Unit]] = Seq.empty
11 | ): Seq[String] = {
12 | println(message)
13 | val argsList = readLine().toLowerCase().split(" ").map(_.trim).filter(_.nonEmpty).toSeq
14 | val validationResults =
15 | argsList
16 | .flatMap(arg => validations.map(validate => validate(arg)))
17 | .fold(Right(())) { (result, current) =>
18 | result match {
19 | case Left(errorResult) =>
20 | current match {
21 | case Left(errorCurrent) => Left(errorResult + ". " + errorCurrent)
22 | case Right(_) => Left(errorResult)
23 | }
24 | case Right(_) => current
25 | }
26 | }
27 | validationResults match {
28 | case Left(errors) =>
29 | println(errors)
30 | askFor(message, validations)
31 | case Right(_) => argsList
32 | }
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/doc/Deployment.md:
--------------------------------------------------------------------------------
1 | # Deployment and Environent Documentation
2 |
3 | ### Deployment to Docker Container
4 |
5 | #### Docker
6 |
7 | To create a local docker Container with the [Native Packager](https://github.com/sbt/sbt-native-packager) Plugin:
8 |
9 | The service needs a Postgis Database to generate the Slick Objects and Tables.
10 | It could be started with:
11 |
12 | ```bash
13 | docker run -d -p 5432:5432 -e POSTGRES_PASSWORD=test -e POSTGRES_DB=test -e POSTGRES_USER=test --name bootstrapPlay2PGBuild mdillon/postgis:latest
14 | ```
15 |
16 | ```bash
17 | docker:publishlocal
18 | ```
19 |
20 | ### EnvVars for Configuration
21 |
22 | - DATABASE_DB = Database Endpoint (for example play)
23 | - DATABASE_HOST = Database Host (for example localhost)
24 | - DATABASE_PORT = Database Port
25 | - DATABASE_USER = Database User
26 | - DATABASE_PASSWORD = Database Password
27 | - FIREBASE_JSON = Authentication Json from Google Firebase
28 | - FIREBASE_FILEPATH = Path to Firebase.json (Dont use in CI)
29 |
30 | - HOME = HomePath
31 | - GOOGLE_PROJECT_ID
32 | - GOOGLE_COMPUTE_ZONE
33 | - GOOGLE_CLUSTER_NAME
34 | - GCLOUD_SERVICE_KEY
35 |
36 | In the Terminal those can be set by:
37 |
38 | ```bash
39 | export ENV_VAR=Variable
40 | ```
--------------------------------------------------------------------------------
/doc/FilterDoc.md:
--------------------------------------------------------------------------------
1 | ## AccessFilter Documentation
2 | ###### Last Updated: 17.06.202
3 |
4 | Official Play Documentation: [Play Filters Documentation 2.8](https://www.playframework.com/documentation/2.8.x/Filters)
5 |
6 |
7 |
8 | Filters are defined in [application.conf](../conf/application.conf):
9 |
10 | play.de.innfactory.bootstrapplay2.filters.enabled = [
11 | "de.innfactory.bootstrapplay2.filters.logging.AccessLoggingFilter",
12 | "de.innfactory.bootstrapplay2.filters.access.RouteBlacklistFilter",
13 | "play.de.innfactory.bootstrapplay2.filters.cors.CORSFilter"
14 | ]
15 |
16 | ## Filters:
17 |
18 | ### AccessLoggingFilter
19 | [Go To File](../app/de/innfactory/bootstrapplay2/filters/logging/AccessLoggingFilter.scala)
20 |
21 | - Logs all requests where the statusCode is contained in [application.conf](../conf/application.conf):
22 |
23 | logging.access.statusList = [404,403,401]
24 | logging.access.statusList = ${?LOGGING_STATUSLIST}
25 |
26 | ### RouteBlacklistFilter
27 |
28 | [Go To File](../app/de/innfactory/bootstrapplay2/filters/access/RouteBlacklistFilter.scala)
29 |
30 | - Blocks all requests defined in RouteBlacklistFilter.scala
31 |
32 |
33 |
34 |
--------------------------------------------------------------------------------
/project/plugins.sbt:
--------------------------------------------------------------------------------
1 | resolvers += "Typesafe repository" at "https://repo.typesafe.com/typesafe/releases/"
2 | resolvers += "Sonatype OSS Snapshots" at "https://oss.sonatype.org/content/repositories/snapshots"
3 | resolvers += "Flyway" at "https://davidmweber.github.io/flyway-sbt.repo"
4 | resolvers += Resolver.url("play-sbt-plugins", url("https://dl.bintray.com/playframework/sbt-plugin-releases/"))(
5 | Resolver.ivyStylePatterns
6 | )
7 |
8 | // Database migration
9 | addSbtPlugin("io.github.davidmweber" % "flyway-sbt" % "7.4.0")
10 |
11 | // Slick code generation
12 | addSbtPlugin("com.github.tototoshi" % "sbt-slick-codegen" % "2.2.0")
13 |
14 | // The Play plugin
15 | addSbtPlugin("com.typesafe.play" % "sbt-plugin" % "2.9.6")
16 | addSbtPlugin("org.scoverage" % "sbt-scoverage" % "2.2.2")
17 | addSbtPlugin("com.github.sbt" % "sbt-native-packager" % "1.10.4")
18 |
19 | addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.5.2")
20 | addSbtPlugin("io.github.play-swagger" % "sbt-play-swagger" % "2.0.4")
21 |
22 | addSbtPlugin("com.github.sbt" % "sbt-license-report" % "1.7.0")
23 |
24 | addSbtPlugin("com.codecommit" % "sbt-github-packages" % "0.5.3")
25 |
26 | addSbtPlugin("com.disneystreaming.smithy4s" % "smithy4s-sbt-codegen" % "0.17.19")
27 |
--------------------------------------------------------------------------------
/app/de/innfactory/bootstrapplay2/commons/jwt/JWTValidator.scala:
--------------------------------------------------------------------------------
1 | package de.innfactory.bootstrapplay2.commons.jwt
2 |
3 | import com.nimbusds.jwt.JWTClaimsSet
4 | import com.nimbusds.jwt.proc.BadJWTException
5 | import de.innfactory.play.smithy4play.JWTToken
6 |
7 | sealed abstract class ValidationError(message: String) extends BadJWTException(message)
8 | case object EmptyJwtTokenContent extends ValidationError("Empty JWT token")
9 | case object InvalidRemoteJwkSet extends ValidationError("Cannot retrieve remote JWK set")
10 | case object InvalidJwtToken extends ValidationError("Invalid JWT token")
11 | case object MissingExpirationClaim extends ValidationError("Missing `exp` claim")
12 | case object InvalidTokenUseClaim extends ValidationError("Invalid `token_use` claim")
13 | case object InvalidTokenIssuerClaim extends ValidationError("Invalid `iss` claim")
14 | case object InvalidTokenSubject extends ValidationError("Invalid `sub` claim")
15 | case object InvalidAudienceClaim extends ValidationError("Invalid `aud` claim")
16 | case class UnknownException(exception: Exception) extends ValidationError(exception.getMessage)
17 |
18 | trait JWTValidator {
19 | def validate(jwtToken: JWTToken): Either[BadJWTException, (JWTToken, JWTClaimsSet)]
20 | }
21 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /.idea
2 |
3 | modules/play/logs/
4 |
5 | # setup/bootstrap script
6 | .bin/setup/.bsp
7 | .bin/setup/.idea
8 | .bin/setup/target
9 | .bin/setup/project/target
10 | .bin/schemaspy
11 | !.bin/schemaspy/schemaspy.properties
12 |
13 | # openapi yaml generation
14 | .bin/mock/yaml
15 | openapi-merge.json
16 | output.swagger.json
17 |
18 | # sbt specific
19 | .cache
20 | .history
21 | .lib/
22 | dist/*
23 | target/
24 | lib_managed/
25 | src_managed/
26 | project/boot/
27 | project/plugins/project/
28 |
29 | # Scala-IDE specific
30 | .scala_dependencies
31 | .worksheet
32 |
33 | ### PlayFramework template
34 | # Ignore Play! working directory #
35 | bin/
36 | /db
37 | .eclipse
38 | /lib/
39 | /logs/
40 | /project/project
41 | /project/target
42 | /target
43 | tmp/
44 | test-result
45 | server.pid
46 | *.iml
47 | *.eml
48 | /dist/
49 |
50 | test.mv.db
51 |
52 | #Firebase
53 | conf/firebase.json
54 | conf/gcp-backend-sa.json
55 |
56 | influx2.properties
57 |
58 | #PubSub
59 | pubSub.json
60 | pubSub-dev.json
61 | .circleci/k8sdeploy.yaml
62 | .circleci/k8sdeploy.yaml-e
63 | .metals/*
64 |
65 | .bsp/*
66 |
67 | #puml
68 | out/doc
69 |
70 | .DS_Store
71 |
72 | .bin/postgis-volume
73 | .bloop/
74 |
75 | modules/api/src/main/scala/*
76 | *.smithy.lsp.log
--------------------------------------------------------------------------------
/app/de/innfactory/bootstrapplay2/users/infrastructure/mappers/KeycloakUserMapper.scala:
--------------------------------------------------------------------------------
1 | package de.innfactory.bootstrapplay2.users.infrastructure.mappers
2 |
3 | import de.innfactory.bootstrapplay2.application.keycloak.domain.models.KeycloakUser
4 | import de.innfactory.bootstrapplay2.users.domain.models.{Claims, User, UserId}
5 |
6 | object KeycloakUserMapper {
7 | def keycloakUserToUser(keycloakUser: KeycloakUser): User =
8 | User(
9 | userId = UserId(keycloakUser.id),
10 | email = keycloakUser.email.getOrElse(""),
11 | emailVerified = keycloakUser.emailVerified,
12 | disabled = !keycloakUser.enabled,
13 | firstName = keycloakUser.firstName,
14 | lastName = keycloakUser.lastName,
15 | displayName = (keycloakUser.firstName, keycloakUser.lastName) match {
16 | case (Some(firstName), Some(lastName)) => Some(s"$firstName $lastName")
17 | case (Some(firstName), None) => Some(firstName)
18 | case (None, Some(lastName)) => Some(lastName)
19 | case _ => None
20 | },
21 | lastSignIn = None,
22 | lastRefresh = None,
23 | creation = Some(keycloakUser.createdTimestamp),
24 | claims = Claims()
25 | )
26 | }
27 |
--------------------------------------------------------------------------------
/.github/workflows/openapi-yaml-gen.yml:
--------------------------------------------------------------------------------
1 | name: Run Tests
2 | on:
3 | push:
4 | branches: [ 'master' ]
5 | pull_request:
6 | branches: [ 'master' ]
7 | jobs:
8 | generateSwagger:
9 | name: Run swagger generation
10 | runs-on: ubuntu-latest
11 | steps:
12 | - uses: actions/checkout@v2
13 | - name: Setup JDK
14 | uses: actions/setup-java@v3
15 | with:
16 | distribution: zulu
17 | java-version: 11
18 | - name: Compile project
19 | env:
20 | GITHUB_TOKEN: ${{ secrets.PUBLIC_GITHUB_TOKEN }}
21 | run: sbt compile || true
22 | - name: Run merge.sh
23 | run: .bin/swagger/merge.sh
24 | shell: bash
25 | - name: Convert to yaml
26 | uses: openapi-generators/openapitools-generator-action@v1
27 | with:
28 | generator: openapi-yaml
29 | openapi-file: 'output.swagger.json'
30 | - name: Move converted yaml
31 | run: mv openapi-yaml-client/openapi/openapi.yaml doc-assets/api.swagger.yaml
32 | - name: Update Pull Request with generated swagger file
33 | uses: EndBug/add-and-commit@v9
34 | with:
35 | message: 'feat: update api.swagger.yaml'
36 | add: 'doc-assets/api.swagger.yaml'
--------------------------------------------------------------------------------
/test/controllers/CompaniesGraphqlControllerTest.scala:
--------------------------------------------------------------------------------
1 | package controllers
2 |
3 | import org.scalatestplus.play.{BaseOneAppPerSuite, PlaySpec}
4 | import play.api.libs.json._
5 | import play.api.test.Helpers._
6 | import testutils.BaseFakeRequest
7 | import testutils.BaseFakeRequest._
8 | import testutils.grapqhl.CompanyRequests
9 | import testutils.grapqhl.FakeGraphQLRequest.{getFake, routeResult}
10 |
11 | import java.util.UUID
12 |
13 | class CompaniesGraphqlControllerTest extends PlaySpec with BaseOneAppPerSuite with TestApplicationFactory {
14 |
15 | /** ———————————————— */
16 | /** COMPANIES */
17 | /** ———————————————— */
18 | "CompaniesController" must {
19 |
20 | "getAll" in {
21 | val fake =
22 | routeResult(
23 | getFake(
24 | CompanyRequests.CompanyRequest
25 | .getRequest(filter = None)
26 | )
27 | )
28 | status(fake) mustBe 200
29 | }
30 |
31 | "getAll with boolean Filter" in {
32 | val fake =
33 | routeResult(
34 | getFake(
35 | CompanyRequests.CompanyRequest
36 | .getRequest(filter = Some("booleanAttributeEquals=true"))
37 | )
38 | )
39 | status(fake) mustBe 200
40 | }
41 |
42 | }
43 |
44 | }
45 |
--------------------------------------------------------------------------------
/test/controllers/HealthControllerTest.scala:
--------------------------------------------------------------------------------
1 | package controllers
2 |
3 | import de.innfactory.bootstrapplay2.api.HealthAPIControllerGen
4 | import de.innfactory.smithy4play.client.GenericAPIClient.EnhancedGenericAPIClient
5 | import org.scalatestplus.play.{BaseOneAppPerSuite, PlaySpec}
6 | import testutils.FakeRequestClient
7 | import de.innfactory.smithy4play.client.SmithyPlayTestUtils._
8 |
9 | class HealthControllerTest extends PlaySpec with BaseOneAppPerSuite with TestApplicationFactory {
10 | private val publicClient = HealthAPIControllerGen.withClient(new FakeRequestClient())
11 |
12 | /** ————————————————— */
13 | /** HEALTH CONTROLLER */
14 | /** ————————————————— */
15 | "HealthController" should {
16 | "accept GET request on base path" in {
17 | val result = publicClient.ping().run(None).awaitRight
18 | result.statusCode mustBe result.expectedStatusCode
19 | }
20 | "accept GET request on liveness check path" in {
21 | val result = publicClient.liveness().run(None).awaitRight
22 | result.statusCode mustBe result.expectedStatusCode
23 | }
24 | "accept GET request on readiness check path" in {
25 | val result = publicClient.readiness().run(None).awaitRight
26 | result.statusCode mustBe result.expectedStatusCode
27 | }
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/test/resources/application.conf:
--------------------------------------------------------------------------------
1 | test = {
2 | database = {
3 | url = "jdbc:postgresql://localhost:5432/test"
4 | user = ${?DATABASE_USER}
5 | password = ${?DATABASE_PASSWORD}
6 | driver = org.postgresql.Driver
7 | urlPrefix = "jdbc:postgresql://"
8 | host = "localhost"
9 | host = ${?DATABASE_HOST}
10 | port = "5432"
11 | port = ${?DATABASE_PORT}
12 | db = "test"
13 | db = ${?DATABASE_DB}
14 | testUrl = ${test.database.urlPrefix}${test.database.host}":"${test.database.port}"/"${test.database.db}
15 | testUser = "test"
16 | testUser = ${?DATABASE_USER}
17 | testPassword = "test"
18 | testPassword = ${?DATABASE_PASSWORD}
19 | user = "test"
20 | password = "test"
21 | user = ${?DATABASE_USER}
22 | password = ${?DATABASE_PASSWORD}
23 | url = "jdbc:postgresql://"${?test.database.host}":"${?test.database.port}"/"${?test.database.db}
24 | }
25 | }
26 |
27 | smithy4play.autoRoutePackage = "de.innfactory.bootstrapplay2"
28 |
29 | akka.cluster.seed-nodes = [ "akka://application@127.0.0.1:25520" ]
30 |
31 | akka {
32 | remote {
33 | artery {
34 | transport = tcp # See Selecting a transport below
35 | canonical.hostname = "127.0.0.1"
36 | canonical.port = 25520
37 | }
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/app/de/innfactory/bootstrapplay2/users/domain/models/UserPasswordResetToken.scala:
--------------------------------------------------------------------------------
1 | package de.innfactory.bootstrapplay2.users.domain.models
2 |
3 | import org.apache.commons.codec.binary.Base64
4 | import org.joda.time.DateTime
5 |
6 | import java.nio.charset.StandardCharsets
7 | import java.security.{MessageDigest, SecureRandom}
8 | import scala.concurrent.duration.{Duration, DurationInt}
9 |
10 | case class UserPasswordResetToken(
11 | userId: UserId,
12 | token: String,
13 | created: org.joda.time.DateTime,
14 | validUntil: org.joda.time.DateTime
15 | )
16 |
17 | object UserPasswordResetToken {
18 |
19 | private val validity: Duration = 7.days
20 |
21 | def apply(userId: UserId): UserPasswordResetToken = {
22 | val encoded: String = createResetTokenString(userId)
23 | UserPasswordResetToken(
24 | userId,
25 | encoded,
26 | DateTime.now(),
27 | DateTime.now().plus(validity._1)
28 | )
29 | }
30 |
31 | private def createResetTokenString(userId: UserId) = {
32 | val random = new SecureRandom()
33 | val randomBytes = random.generateSeed(10)
34 | val digest = MessageDigest.getInstance("SHA-256")
35 | val hash = digest.digest(userId.value.getBytes(StandardCharsets.UTF_8) ++ randomBytes)
36 | val encoded = Base64.encodeBase64URLSafeString(hash)
37 | encoded
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/conf/akka.conf:
--------------------------------------------------------------------------------
1 | akka.serialization.jackson {
2 | jackson-modules += "com.fasterxml.jackson.datatype.joda.JodaModule"
3 | jackson-modules += "de.innfactory.bootstrapplay2.commons.jackson.JsValueDeSerializerModule"
4 | }
5 |
6 | akka.cluster.seed-nodes = [ ]
7 | akka.cluster.seed-nodes = ${?AKKA_SEED_NODES}
8 |
9 | akka {
10 | loglevel = "INFO"
11 | actor {
12 | provider = "cluster"
13 | debug.receive = false
14 | }
15 | cluster {
16 | downing-provider-class = "akka.cluster.sbr.SplitBrainResolverProvider"
17 | shutdown-after-unsuccessful-join-seed-nodes = 60s
18 | }
19 | }
20 |
21 |
22 | akka.cluster.log-info-verbose = off
23 |
24 | akka.management {
25 | cluster.bootstrap {
26 | contact-point-discovery {
27 | discovery-method = kubernetes-api
28 | }
29 | }
30 | }
31 | akka.discovery {
32 | kubernetes-api {
33 | pod-namespace = "dev"
34 | pod-namespace = ${?NAMESPACE}
35 | pod-label-selector = "appName=bootstrapplay2"
36 | }
37 | }
38 |
39 | akka.actor {
40 | allow-java-serialization = off
41 | serializers {
42 | jackson-json-event = "akka.serialization.jackson.JacksonJsonSerializer"
43 | }
44 | serialization-identifiers {
45 | jackson-json-event = 9001
46 | }
47 | serialization-bindings {
48 | "play.api.libs.json.JsValue" = jackson-json
49 | }
50 | }
--------------------------------------------------------------------------------
/app/de/innfactory/bootstrapplay2/graphql/schema/models/Companies.scala:
--------------------------------------------------------------------------------
1 | package de.innfactory.bootstrapplay2.graphql.schema.models
2 |
3 | import de.innfactory.bootstrapplay2.commons.RequestContext
4 | import de.innfactory.bootstrapplay2.companies.domain.models.Company
5 | import de.innfactory.bootstrapplay2.graphql.GraphQLExecutionContext
6 | import de.innfactory.bootstrapplay2.graphql.RequestExecutor.EnhancedRequest
7 | import sangria.macros.derive.{deriveObjectType, AddFields, ReplaceField}
8 | import sangria.schema.{Field, ListType, ObjectType, StringType}
9 | import de.innfactory.grapqhl.sangria.implicits.JsonScalarType._
10 | import de.innfactory.grapqhl.sangria.implicits.JodaScalarType._
11 |
12 | object Companies {
13 | val CompanyType: ObjectType[GraphQLExecutionContext, Company] = deriveObjectType(
14 | ReplaceField(
15 | fieldName = "id",
16 | field = Field(name = "id", fieldType = StringType, resolve = c => c.value.id.value)
17 | ),
18 | AddFields(
19 | Field(
20 | name = "subcompanies",
21 | resolve = ctx =>
22 | ctx.ctx.request.toRequestContextAndExecute(
23 | "allCompanies GraphQL",
24 | (rc: RequestContext) => ctx.ctx.companiesService.getAllForGraphQL(None)(rc)
25 | )(ctx.ctx.ec),
26 | fieldType = ListType(CompanyType)
27 | )
28 | )
29 | )
30 | }
31 |
--------------------------------------------------------------------------------
/app/de/innfactory/bootstrapplay2/locations/domain/models/Location.scala:
--------------------------------------------------------------------------------
1 | package de.innfactory.bootstrapplay2.locations.domain.models
2 |
3 | import de.innfactory.bootstrapplay2.companies.domain.models.CompanyId
4 | import de.innfactory.implicits.OptionUtils.EnhancedOption
5 | import org.joda.time.DateTime
6 | import play.api.libs.json._
7 |
8 | case class Location(
9 | id: LocationId,
10 | company: CompanyId,
11 | name: Option[String],
12 | settings: Option[JsValue],
13 | addressLine1: Option[String],
14 | addressLine2: Option[String],
15 | zip: Option[String],
16 | city: Option[String],
17 | country: Option[String],
18 | created: DateTime,
19 | updated: DateTime
20 | ) {
21 | def patch(newObject: Location): Location =
22 | newObject.copy(
23 | id = this.id,
24 | company = this.company,
25 | name = newObject.name.getOrElseOld(this.name),
26 | addressLine1 = newObject.addressLine1.getOrElseOld(this.addressLine1),
27 | addressLine2 = newObject.addressLine2.getOrElseOld(this.addressLine2),
28 | zip = newObject.zip.getOrElseOld(this.zip),
29 | city = newObject.city.getOrElseOld(this.city),
30 | country = newObject.country.getOrElseOld(this.country),
31 | settings = newObject.settings.getOrElseOld(this.settings),
32 | created = this.created,
33 | updated = DateTime.now
34 | )
35 | }
36 |
--------------------------------------------------------------------------------
/app/de/innfactory/bootstrapplay2/locations/application/mapper/LocationMapper.scala:
--------------------------------------------------------------------------------
1 | package de.innfactory.bootstrapplay2.locations.application.mapper
2 |
3 | import de.innfactory.bootstrapplay2.application.controller.BaseMapper
4 | import de.innfactory.bootstrapplay2.api.{LocationRequestBody, LocationResponse, LocationsResponse}
5 | import de.innfactory.bootstrapplay2.companies.domain.models.CompanyId._
6 | import de.innfactory.bootstrapplay2.locations.domain.models.{Location, LocationId}
7 | import io.scalaland.chimney.dsl.TransformerOps
8 | import org.joda.time.DateTime
9 |
10 | trait LocationMapper extends BaseMapper {
11 | implicit val locationToLocationResponse: Location => LocationResponse = (location: Location) =>
12 | location
13 | .into[LocationResponse]
14 | .transform
15 |
16 | implicit val locationsToLocationsResponse: Seq[Location] => LocationsResponse = (locations: Seq[Location]) =>
17 | LocationsResponse(locations.map(locationToLocationResponse))
18 |
19 | implicit val locationRequestBodyToLocation: LocationRequestBody => Location =
20 | (locationRequestBody: LocationRequestBody) =>
21 | locationRequestBody
22 | .into[Location]
23 | .withFieldComputed(_.id, l => l.id.map(LocationId.locationIdToDomain).getOrElse(LocationId.create))
24 | .withFieldConst(_.created, DateTime.now())
25 | .withFieldConst(_.updated, DateTime.now())
26 | .transform
27 | }
28 |
--------------------------------------------------------------------------------
/.bin/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | firebase-debug.log*
8 | firebase-debug.*.log*
9 |
10 | # Firebase cache
11 | .firebase/
12 |
13 | # Firebase config
14 |
15 | # Uncomment this if you'd like others to create their own Firebase project.
16 | # For a team working on the same Firebase project(s), it is recommended to leave
17 | # it commented so all members can deploy to the same project(s) in .firebaserc.
18 | # .firebaserc
19 |
20 | # Runtime data
21 | pids
22 | *.pid
23 | *.seed
24 | *.pid.lock
25 |
26 | # Directory for instrumented libs generated by jscoverage/JSCover
27 | lib-cov
28 |
29 | # Coverage directory used by tools like istanbul
30 | coverage
31 |
32 | # nyc test coverage
33 | .nyc_output
34 |
35 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
36 | .grunt
37 |
38 | # Bower dependency directory (https://bower.io/)
39 | bower_components
40 |
41 | # node-waf configuration
42 | .lock-wscript
43 |
44 | # Compiled binary addons (http://nodejs.org/api/addons.html)
45 | build/Release
46 |
47 | # Dependency directories
48 | node_modules/
49 |
50 | # Optional npm cache directory
51 | .npm
52 |
53 | # Optional eslint cache
54 | .eslintcache
55 |
56 | # Optional REPL history
57 | .node_repl_history
58 |
59 | # Output of 'npm pack'
60 | *.tgz
61 |
62 | # Yarn Integrity file
63 | .yarn-integrity
64 |
65 | # dotenv environment variables file
66 | .env
67 |
--------------------------------------------------------------------------------
/app/de/innfactory/bootstrapplay2/application/controller/HealthController.scala:
--------------------------------------------------------------------------------
1 | package de.innfactory.bootstrapplay2.application.controller
2 |
3 | import cats.data.EitherT
4 | import com.typesafe.config.Config
5 | import de.innfactory.bootstrapplay2.api.HealthAPIController
6 | import de.innfactory.bootstrapplay2.commons.infrastructure.DatabaseHealthSocket
7 | import de.innfactory.play.results.errors.Errors.InternalServerError
8 | import de.innfactory.play.smithy4play.ImplicitLogContext
9 | import de.innfactory.smithy4play.{AutoRouting, ContextRoute}
10 | import play.api.Application
11 | import play.api.mvc.ControllerComponents
12 |
13 | import javax.inject.{Inject, Singleton}
14 | import scala.concurrent.ExecutionContext
15 |
16 | @AutoRouting
17 | @Singleton
18 | class HealthController @Inject() (
19 | databaseHealthSocket: DatabaseHealthSocket
20 | )(implicit ec: ExecutionContext, cc: ControllerComponents, app: Application, config: Config)
21 | extends BaseController
22 | with ImplicitLogContext
23 | with HealthAPIController[ContextRoute] {
24 |
25 | def ping(): ContextRoute[Unit] = Endpoint
26 | .execute(_ =>
27 | EitherT.rightT(
28 | if (databaseHealthSocket.isConnectionOpen) Right("")
29 | else Left(InternalServerError("Database Connection Lost"))
30 | )
31 | )
32 | .complete
33 |
34 | override def liveness(): ContextRoute[Unit] = ping()
35 |
36 | override def readiness(): ContextRoute[Unit] = ping()
37 | }
38 |
--------------------------------------------------------------------------------
/app/de/innfactory/bootstrapplay2/companies/application/mapper/CompanyMapper.scala:
--------------------------------------------------------------------------------
1 | package de.innfactory.bootstrapplay2.companies.application.mapper
2 |
3 | import de.innfactory.bootstrapplay2.application.controller.BaseMapper
4 | import de.innfactory.bootstrapplay2.companies.domain.models.{Company, CompanyId}
5 | import de.innfactory.bootstrapplay2.api.{CompaniesResponse, CompanyRequestBody, CompanyResponse}
6 | import io.scalaland.chimney.dsl.TransformerOps
7 | import org.joda.time.DateTime
8 |
9 | trait CompanyMapper extends BaseMapper {
10 | implicit val companyToCompanyResponse: Company => CompanyResponse = (
11 | company: Company
12 | ) =>
13 | company
14 | .into[CompanyResponse]
15 | .withFieldComputed(_.created, c => dateTimeToDateWithTime(c.created))
16 | .withFieldComputed(_.updated, c => dateTimeToDateWithTime(c.updated))
17 | .transform
18 |
19 | implicit val companiesToCompaniesResponse: Seq[Company] => CompaniesResponse = (companies: Seq[Company]) =>
20 | CompaniesResponse(companies.map(companyToCompanyResponse))
21 |
22 | implicit val companyRequestBodyToCompany: CompanyRequestBody => Company = (companyRequestBody: CompanyRequestBody) =>
23 | companyRequestBody
24 | .into[Company]
25 | .withFieldComputed(_.id, c => c.id.map(CompanyId.companyIdToDomain).getOrElse(CompanyId.create))
26 | .withFieldConst(_.created, DateTime.now())
27 | .withFieldConst(_.updated, DateTime.now())
28 | .transform
29 | }
30 |
--------------------------------------------------------------------------------
/app/de/innfactory/bootstrapplay2/locations/domain/interfaces/LocationRepository.scala:
--------------------------------------------------------------------------------
1 | package de.innfactory.bootstrapplay2.locations.domain.interfaces
2 |
3 | import akka.NotUsed
4 | import akka.stream.scaladsl.Source
5 | import cats.data.EitherT
6 | import com.google.inject.ImplementedBy
7 | import de.innfactory.bootstrapplay2.companies.domain.models.CompanyId
8 | import de.innfactory.play.tracing.TraceContext
9 | import de.innfactory.play.controller.ResultStatus
10 | import de.innfactory.bootstrapplay2.locations.domain.models.{Location, LocationId}
11 | import de.innfactory.bootstrapplay2.locations.infrastructure.SlickLocationRepository
12 |
13 | import scala.concurrent.Future
14 |
15 | @ImplementedBy(classOf[SlickLocationRepository])
16 | private[locations] trait LocationRepository {
17 |
18 | def getAllLocationsByCompany(companyId: CompanyId)(implicit
19 | rc: TraceContext
20 | ): EitherT[Future, ResultStatus, Seq[Location]]
21 |
22 | def getAllLocationsAsSource(implicit rc: TraceContext): Source[Location, NotUsed]
23 |
24 | def getById(locationId: LocationId)(implicit rc: TraceContext): EitherT[Future, ResultStatus, Location]
25 |
26 | def createLocation(location: Location)(implicit rc: TraceContext): EitherT[Future, ResultStatus, Location]
27 |
28 | def updateLocation(location: Location)(implicit rc: TraceContext): EitherT[Future, ResultStatus, Location]
29 |
30 | def deleteLocation(locationId: LocationId)(implicit rc: TraceContext): EitherT[Future, ResultStatus, Boolean]
31 |
32 | }
33 |
--------------------------------------------------------------------------------
/test/testutils/AuthUtils.scala:
--------------------------------------------------------------------------------
1 | package testutils
2 |
3 | import com.typesafe.config.Config
4 | import play.api.libs.json.Json
5 | import play.api.libs.ws.WSClient
6 |
7 | import javax.inject.Inject
8 | import scala.concurrent.Await
9 | import scala.concurrent.duration.DurationInt
10 |
11 | class AuthUtils @Inject() (wsClient: WSClient, config: Config) {
12 | import AuthUtils._
13 |
14 | def NotVerifiedEmailToken: String = getTokenForEmail(NotVerifiedEmail)
15 | def CompanyAdminEmailToken: String = getTokenForEmail(CompanyAdminEmail)
16 |
17 | def getTokenFor(email: String) = getTokenForEmail(email)
18 |
19 | private def getTokenForEmail(email: String): String = {
20 | val result = wsClient
21 | .url("http://localhost:9099/identitytoolkit.googleapis.com/v1/accounts:signInWithPassword?key=default")
22 | .post(
23 | Json.parse(
24 | s"""
25 | |{
26 | | "email": "$email",
27 | | "password": "testtest",
28 | | "returnSecureToken":true
29 | |}
30 | |""".stripMargin
31 | )
32 | )
33 | val res = Await.result(result, 5.seconds)
34 | val token = res.json.\("idToken").as[String]
35 | token
36 | }
37 |
38 | }
39 |
40 | object AuthUtils {
41 | val NotVerifiedEmail = "notverified@innfactory.de"
42 | val NotVerifiedEmailId = "WasFqkZRc9TktJmu2DoDSFienga2"
43 | val CompanyAdminEmail = "test@test.de"
44 | val CompanyAdminEmailId = "W88FqkZRc9TktJmu2Dow4xUkT0UU"
45 | }
46 |
--------------------------------------------------------------------------------
/app/de/innfactory/bootstrapplay2/companies/domain/interfaces/CompanyRepository.scala:
--------------------------------------------------------------------------------
1 | package de.innfactory.bootstrapplay2.companies.domain.interfaces
2 |
3 | import akka.NotUsed
4 | import akka.stream.scaladsl.Source
5 | import cats.data.EitherT
6 | import com.google.inject.ImplementedBy
7 | import de.innfactory.bootstrapplay2.commons.RequestContext
8 | import de.innfactory.play.controller.ResultStatus
9 | import de.innfactory.bootstrapplay2.companies.domain.models.{Company, CompanyId}
10 | import de.innfactory.bootstrapplay2.companies.infrastructure.SlickCompanyRepository
11 | import de.innfactory.play.tracing.TraceContext
12 |
13 | import scala.concurrent.Future
14 |
15 | @ImplementedBy(classOf[SlickCompanyRepository])
16 | private[companies] trait CompanyRepository {
17 |
18 | def getAll()(implicit rc: TraceContext): EitherT[Future, ResultStatus, Seq[Company]]
19 |
20 | def getAllForGraphQL(filterOptions: Option[String])(implicit rc: RequestContext): Future[Seq[Company]]
21 |
22 | def getAllCompaniesAsSource(implicit rc: TraceContext): Source[Company, NotUsed]
23 |
24 | def getById(companyId: CompanyId)(implicit rc: TraceContext): EitherT[Future, ResultStatus, Company]
25 |
26 | def createCompany(company: Company)(implicit rc: TraceContext): EitherT[Future, ResultStatus, Company]
27 |
28 | def updateCompany(company: Company)(implicit rc: TraceContext): EitherT[Future, ResultStatus, Company]
29 |
30 | def deleteCompany(id: CompanyId)(implicit rc: TraceContext): EitherT[Future, ResultStatus, Boolean]
31 |
32 | }
33 |
--------------------------------------------------------------------------------
/app/de/innfactory/bootstrapplay2/locations/domain/interfaces/LocationService.scala:
--------------------------------------------------------------------------------
1 | package de.innfactory.bootstrapplay2.locations.domain.interfaces
2 |
3 | import akka.NotUsed
4 | import akka.stream.scaladsl.Source
5 | import cats.data.EitherT
6 | import com.google.inject.ImplementedBy
7 | import de.innfactory.bootstrapplay2.commons.RequestContextWithUser
8 | import de.innfactory.bootstrapplay2.companies.domain.models.CompanyId
9 | import de.innfactory.play.controller.ResultStatus
10 | import de.innfactory.bootstrapplay2.locations.domain.models.{Location, LocationId}
11 | import de.innfactory.bootstrapplay2.locations.domain.services.DomainLocationService
12 |
13 | import scala.concurrent.Future
14 |
15 | @ImplementedBy(classOf[DomainLocationService])
16 | trait LocationService {
17 |
18 | def getAllByCompany(CompanyId: CompanyId)(implicit
19 | rc: RequestContextWithUser
20 | ): EitherT[Future, ResultStatus, Seq[Location]]
21 |
22 | def getAllAsStream()(implicit rc: RequestContextWithUser): EitherT[Future, ResultStatus, Source[Location, NotUsed]]
23 |
24 | def getById(id: LocationId)(implicit rc: RequestContextWithUser): EitherT[Future, ResultStatus, Location]
25 |
26 | def updateLocation(company: Location)(implicit rc: RequestContextWithUser): EitherT[Future, ResultStatus, Location]
27 |
28 | def createLocation(company: Location)(implicit rc: RequestContextWithUser): EitherT[Future, ResultStatus, Location]
29 |
30 | def deleteLocation(id: LocationId)(implicit rc: RequestContextWithUser): EitherT[Future, ResultStatus, Boolean]
31 |
32 | }
33 |
--------------------------------------------------------------------------------
/app/de/innfactory/bootstrapplay2/companies/domain/interfaces/CompanyService.scala:
--------------------------------------------------------------------------------
1 | package de.innfactory.bootstrapplay2.companies.domain.interfaces
2 |
3 | import akka.NotUsed
4 | import akka.stream.scaladsl.Source
5 | import cats.data.EitherT
6 | import com.google.inject.ImplementedBy
7 | import de.innfactory.bootstrapplay2.commons.{RequestContext, RequestContextWithUser}
8 | import de.innfactory.bootstrapplay2.companies.domain.models.{Company, CompanyId}
9 | import de.innfactory.bootstrapplay2.companies.domain.services.DomainCompanyService
10 | import de.innfactory.play.controller.ResultStatus
11 |
12 | import scala.concurrent.Future
13 |
14 | @ImplementedBy(classOf[DomainCompanyService])
15 | trait CompanyService {
16 |
17 | def getAll()(implicit rc: RequestContextWithUser): EitherT[Future, ResultStatus, Seq[Company]]
18 |
19 | def getAllForGraphQL(filterOptions: Option[String])(implicit rc: RequestContext): Future[Seq[Company]]
20 |
21 | def getAllCompaniesAsStream()(implicit
22 | rc: RequestContextWithUser
23 | ): EitherT[Future, ResultStatus, Source[Company, NotUsed]]
24 |
25 | def getById(id: CompanyId)(implicit rc: RequestContextWithUser): EitherT[Future, ResultStatus, Company]
26 |
27 | def updateCompany(company: Company)(implicit rc: RequestContextWithUser): EitherT[Future, ResultStatus, Company]
28 |
29 | def createCompany(company: Company)(implicit rc: RequestContextWithUser): EitherT[Future, ResultStatus, Company]
30 |
31 | def deleteCompany(id: CompanyId)(implicit rc: RequestContextWithUser): EitherT[Future, ResultStatus, Boolean]
32 |
33 | }
34 |
--------------------------------------------------------------------------------
/app/de/innfactory/bootstrapplay2/users/domain/interfaces/UserRepository.scala:
--------------------------------------------------------------------------------
1 | package de.innfactory.bootstrapplay2.users.domain.interfaces
2 |
3 | import akka.NotUsed
4 | import akka.stream.scaladsl.Source
5 | import cats.data.EitherT
6 | import com.google.inject.ImplementedBy
7 | import de.innfactory.bootstrapplay2.users.domain.models.{Claims, User, UserId}
8 | import de.innfactory.bootstrapplay2.users.infrastructure.UserRepositoryMock
9 | import de.innfactory.play.controller.ResultStatus
10 |
11 | import scala.concurrent.Future
12 |
13 | @ImplementedBy(classOf[UserRepositoryMock])
14 | trait UserRepository {
15 | def getAllUsersAsSource: Source[User, NotUsed]
16 |
17 | def getUserByEmail(email: String): EitherT[Future, ResultStatus, User]
18 |
19 | def getUserById(userId: UserId): EitherT[Future, ResultStatus, User]
20 |
21 | def createUser(email: String): EitherT[Future, ResultStatus, User]
22 |
23 | def upsertUser(user: User, oldUser: User): EitherT[Future, ResultStatus, User]
24 |
25 | def setUserClaims(userId: UserId, claims: Claims): EitherT[Future, ResultStatus, Boolean]
26 |
27 | def setUserPassword(userId: UserId, newPassword: String): EitherT[Future, ResultStatus, User]
28 |
29 | def setEmailVerifiedState(userId: UserId, state: Boolean): EitherT[Future, ResultStatus, Boolean]
30 |
31 | def setUserDisabledState(userId: UserId, state: Boolean): EitherT[Future, ResultStatus, Boolean]
32 |
33 | def setUserDisplayName(userId: UserId, displayName: String): EitherT[Future, ResultStatus, Boolean]
34 |
35 | def setEmailVerified(userId: UserId): EitherT[Future, ResultStatus, Boolean]
36 | }
37 |
--------------------------------------------------------------------------------
/app/de/innfactory/bootstrapplay2/commons/filteroptions/FilterOptionUtils.scala:
--------------------------------------------------------------------------------
1 | package de.innfactory.bootstrapplay2.commons.filteroptions
2 |
3 | import dbdata.Tables
4 | import de.innfactory.play.slick.enhanced.utils.filteroptions.FilterOptions
5 |
6 | object FilterOptionUtils {
7 |
8 | private def queryStringToOptionsSequence(implicit
9 | queryString: Map[String, Seq[String]]
10 | ): Seq[FilterOptions[Tables.Company, _]] = {
11 | val filterOptionsConfig = new FilterOptionsConfig
12 | filterOptionsConfig.companiesFilterOptions
13 | .map(_.getFromQueryString(queryString))
14 | .filter(_.isDefined)
15 | .map(_.get)
16 | .filter(_.atLeasOneFilterOptionApplicable)
17 | }
18 |
19 | /**
20 | * Map Query String to Filter Options
21 | * @param queryString
22 | */
23 | def queryStringToFilterOptions(implicit
24 | queryString: Map[String, Seq[String]]
25 | ): Seq[FilterOptions[Tables.Company, _]] = queryStringToOptionsSequence(queryString)
26 |
27 | def optionStringToFilterOptions(implicit
28 | optionString: Option[String]
29 | ): Seq[FilterOptions[Tables.Company, _]] =
30 | optionString match {
31 | case Some(value) if !value.isBlank =>
32 | val query: Map[String, Seq[String]] = value
33 | .split('&')
34 | .map(_.split('='))
35 | .map(array => array.head -> array.tail.head)
36 | .groupBy(_._1)
37 | .map(e =>
38 | e._1 -> e._2
39 | .map(_._2)
40 | .toSeq
41 | )
42 | queryStringToFilterOptions(query)
43 | case _ => Seq.empty
44 | }
45 |
46 | }
47 |
--------------------------------------------------------------------------------
/app/de/innfactory/bootstrapplay2/application/controller/BaseMapper.scala:
--------------------------------------------------------------------------------
1 | package de.innfactory.bootstrapplay2.application.controller
2 |
3 | import de.innfactory.play.smithy4play.PlayJsonToDocumentMapper
4 | import io.scalaland.chimney.Transformer
5 | import org.joda.time.DateTime
6 | import play.api.libs.json.JsValue
7 | import smithy4s.Document
8 | import de.innfactory.bootstrapplay2.api.DateWithTime
9 |
10 | import scala.language.implicitConversions
11 |
12 | trait BaseMapper {
13 |
14 | implicit val transformJsValueToDocument: Transformer[JsValue, Document] = PlayJsonToDocumentMapper.mapToDocument
15 | implicit val transformDocumentToJsValue: Transformer[Document, JsValue] = PlayJsonToDocumentMapper.documentToJsValue
16 |
17 | implicit def unitMapper[T](any: T): Unit = ()
18 |
19 | implicit def dateWithTimeToDateTime(dateWithTime: DateWithTime): DateTime =
20 | DateTime.parse(dateWithTime.value)
21 |
22 | implicit def dateTimeToDateWithTime(dateTime: DateTime): DateWithTime =
23 | DateWithTime(dateTime.toString())
24 |
25 | implicit def optionMapper[T, R](option: Option[T])(implicit optionValueMapper: T => R): Option[R] =
26 | option.map(optionValueMapper)
27 |
28 | implicit def sequenceTransformer[T, R](seq: Seq[T])(implicit transform: T => R): List[R] =
29 | seq.map(transform).toList
30 |
31 | implicit val dateWithTimeToDateTimeTransformer: Transformer[DateWithTime, DateTime] =
32 | (dateWithTime: DateWithTime) => dateWithTimeToDateTime(dateWithTime)
33 |
34 | implicit val dateTimeToDateWithTimeTransformer: Transformer[DateTime, DateWithTime] =
35 | (dateTime: DateTime) => dateTimeToDateWithTime(dateTime)
36 | }
37 |
--------------------------------------------------------------------------------
/conf/db/migration/V1__Tables.sql:
--------------------------------------------------------------------------------
1 | -- ************************************** "company"
2 |
3 | CREATE TABLE "company"
4 | (
5 | "id" varchar NOT NULL,
6 | "settings" json,
7 | "string_attribute_1" varchar,
8 | "string_attribute_2" varchar,
9 | "long_attribute_1" bigint,
10 | "boolean_attribute" boolean,
11 | "created" timestamp NOT NULL,
12 | "updated" timestamp NOT NULL,
13 | CONSTRAINT "PK_company" PRIMARY KEY ("id")
14 | );
15 |
16 | -- ************************************** "location"
17 |
18 | CREATE TABLE "location"
19 | (
20 | "id" varchar NOT NULL,
21 | "company" varchar NOT NULL,
22 | "name" varchar(255),
23 | "settings" json,
24 | "address_line_1" varchar(300),
25 | "address_line_2" varchar(300),
26 | "zip" varchar(300),
27 | "city" varchar(300),
28 | "country" varchar(300),
29 | "created" timestamp NOT NULL,
30 | "updated" timestamp NOT NULL,
31 | CONSTRAINT "PK_location" PRIMARY KEY ("id"),
32 | CONSTRAINT "FK_location_company" FOREIGN KEY ("company") REFERENCES "company" ("id") ON DELETE CASCADE
33 | );
34 |
35 |
36 | -- ************************************** "user_password_reset_tokens:"
37 |
38 | CREATE TABLE "user_password_reset_tokens"
39 | (
40 | "user_id" varchar NOT NULL,
41 | "token" varchar NOT NULL,
42 | "created" timestamp NOT NULL,
43 | "valid_until" timestamp NOT NULL,
44 | CONSTRAINT "PK_user_password_reset_tokens" PRIMARY KEY ("user_id")
45 | );
46 |
47 |
48 |
49 |
50 |
--------------------------------------------------------------------------------
/test/testutils/FakeRequestClient.scala:
--------------------------------------------------------------------------------
1 | package testutils
2 |
3 | import akka.stream.Materializer
4 | import de.innfactory.smithy4play.client.{RequestClient, SmithyClientResponse}
5 | import play.api.Application
6 | import play.api.mvc.AnyContentAsEmpty
7 | import play.api.test.FakeRequest
8 | import play.api.test.Helpers._
9 |
10 | import scala.concurrent.{ExecutionContext, Future}
11 |
12 | class FakeRequestClient(implicit application: Application, ec: ExecutionContext, mat: Materializer)
13 | extends RequestClient {
14 | override def send(
15 | method: String,
16 | path: String,
17 | headers: Map[String, Seq[String]],
18 | body: Option[Array[Byte]]
19 | ): Future[SmithyClientResponse] = {
20 | val baseRequest: FakeRequest[AnyContentAsEmpty.type] = FakeRequest(method, path)
21 | .withHeaders(headers.toList.flatMap(headers => headers._2.map(v => (headers._1, v))): _*)
22 | val res =
23 | if (body.isDefined) route(application, baseRequest.withBody(body.get)).get
24 | else
25 | route(
26 | application,
27 | baseRequest
28 | ).get
29 |
30 | for {
31 | result <- res
32 | headers = result.header.headers.map(v => (v._1, Seq(v._2)))
33 | body <- result.body.consumeData.map(_.toArrayUnsafe())
34 | bodyConsumed = if (result.body.isKnownEmpty) None else Some(body)
35 | contentType = result.body.contentType
36 | headersWithContentType =
37 | if (contentType.isDefined) headers + ("Content-Type" -> Seq(contentType.get)) else headers
38 | } yield SmithyClientResponse(bodyConsumed, headersWithContentType, result.header.status)
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/app/de/innfactory/bootstrapplay2/commons/RequestContext.scala:
--------------------------------------------------------------------------------
1 | package de.innfactory.bootstrapplay2.commons
2 | import cats.implicits.toBifunctorOps
3 | import de.innfactory.bootstrapplay2.users.domain.models.{User, UserId}
4 | import de.innfactory.play.smithy4play.HttpHeaders
5 | import de.innfactory.play.tracing.TraceContext
6 | import io.opentelemetry.api.trace.Span
7 | import org.joda.time.DateTime
8 |
9 | import scala.util.Try
10 |
11 | trait ApplicationTraceContext extends TraceContext {
12 |
13 | def timeOverride: Option[DateTime] = Try(
14 | httpHeaders.getHeader("x-app-datetime").map(DateTime.parse)
15 | ).toEither
16 | .leftMap(left => this.log.error("cannot parse x-app-time, because of error " + left.getMessage))
17 | .toOption
18 | .flatten
19 | }
20 |
21 | class RequestContext(
22 | rhttpHeaders: HttpHeaders,
23 | rSpan: Option[Span] = None
24 | ) extends ApplicationTraceContext {
25 | override def httpHeaders: HttpHeaders = rhttpHeaders
26 | override def span: Option[Span] = rSpan
27 | }
28 |
29 | object RequestContext {
30 | implicit def fromRequestContextWithUser(requestContextWithUser: RequestContextWithUser): RequestContext =
31 | new RequestContext(requestContextWithUser.httpHeaders, requestContextWithUser.span)
32 |
33 | def empty = new RequestContext(HttpHeaders(Map.empty))
34 | }
35 |
36 | case class RequestContextWithUser(
37 | override val httpHeaders: HttpHeaders,
38 | override val span: Option[Span],
39 | user: User
40 | ) extends RequestContext(httpHeaders, span)
41 |
42 | object RequestContextWithUser {
43 | implicit def toUserId(requestContextWithUser: RequestContextWithUser): UserId = requestContextWithUser.user.userId
44 | }
45 |
--------------------------------------------------------------------------------
/app/de/innfactory/bootstrapplay2/actorsystem/domain/services/HelloWorldServiceImpl.scala:
--------------------------------------------------------------------------------
1 | package de.innfactory.bootstrapplay2.actorsystem.domain.services
2 |
3 | import akka.actor._
4 | import akka.actor.typed.scaladsl.AskPattern._
5 | import akka.actor.typed.scaladsl.adapter._
6 | import akka.util.Timeout
7 | import de.innfactory.bootstrapplay2.actorsystem.domain.actors.HelloWorldActor
8 | import de.innfactory.bootstrapplay2.actorsystem.domain.commands.{Command, QueryHelloWorld, Response}
9 | import de.innfactory.bootstrapplay2.actorsystem.domain.interfaces.HelloWorldService
10 |
11 | import javax.inject._
12 | import scala.concurrent.duration._
13 | import scala.concurrent.{ExecutionContext, Future}
14 |
15 | @Singleton
16 | class HelloWorldServiceImpl @Inject() (
17 | )(implicit ec: ExecutionContext, system: ActorSystem)
18 | extends HelloWorldService {
19 | // // asking someone requires a timeout if the timeout hits without response
20 | // the ask is failed with a TimeoutException
21 | private implicit val timeout: Timeout = 10.seconds
22 |
23 | // Convert classic actor system of play to typed
24 | private val actorSystem: akka.actor.typed.ActorSystem[_] = system.toTyped
25 | // define implicit scheduler
26 | private implicit val scheduler: akka.actor.typed.Scheduler =
27 | actorSystem.scheduler
28 |
29 | // spawn "root" supervisorActor on new typed actorSystem
30 | private val helloWorldActor: akka.actor.typed.ActorRef[Command] =
31 | actorSystem.systemActorOf(HelloWorldActor(), "helloWorldActor")
32 |
33 | def queryHelloWorld(query: String): Future[Response] = {
34 | val result: Future[Response] =
35 | helloWorldActor.ask((ref: akka.actor.typed.ActorRef[Response]) => QueryHelloWorld(query, ref))
36 | result
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/app/de/innfactory/bootstrapplay2/commons/application/actions/utils/UserUtilsImpl.scala:
--------------------------------------------------------------------------------
1 | package de.innfactory.bootstrapplay2.commons.application.actions.utils
2 |
3 | import cats.data.EitherT
4 | import cats.implicits.catsSyntaxEitherId
5 | import de.innfactory.bootstrapplay2.users.domain.models.{User, UserId}
6 | import de.innfactory.bootstrapplay2.users.domain.services.DomainUserService
7 | import de.innfactory.play.results.errors.Errors.Forbidden
8 | import de.innfactory.play.controller.ResultStatus
9 | import de.innfactory.play.results.Results.Result
10 | import de.innfactory.play.smithy4play.JWTToken
11 | import play.libs.Json
12 |
13 | import java.util.Base64
14 | import javax.inject.Inject
15 | import scala.concurrent.{ExecutionContext, Future}
16 |
17 | class UserUtilsImpl @Inject() (domainUserService: DomainUserService)(implicit ec: ExecutionContext)
18 | extends UserUtils[User] {
19 | def extractUserId(authorizationHeader: Option[JWTToken]): Future[Result[String]] =
20 | try {
21 | val tokenContent = authorizationHeader.get.content.split('.')(1)
22 | val decoded = new String(Base64.getDecoder.decode(tokenContent))
23 | val userId = Json.parse(decoded).get("user_id").asText()
24 | Future(userId.asRight[ResultStatus])
25 | } catch {
26 | case _: Exception => Future(Forbidden("").asLeft[String])
27 | }
28 | def validateJwtToken(authorizationHeader: Option[JWTToken]): Future[Result[Unit]] =
29 | Future(().asRight[ResultStatus])
30 |
31 | override def getUser(authorizationHeader: Option[JWTToken]): Future[Result[User]] = {
32 | for {
33 | userId <- EitherT(extractUserId(authorizationHeader))
34 | user <- domainUserService.getUserByIdWithoutRequestContext(UserId(userId))
35 | } yield user
36 | }.value
37 | }
38 |
--------------------------------------------------------------------------------
/app/de/innfactory/bootstrapplay2/filters/access/RouteBlacklistFilter.scala:
--------------------------------------------------------------------------------
1 | package de.innfactory.bootstrapplay2.filters.access
2 |
3 | import javax.inject.Inject
4 | import akka.stream.Materializer
5 | import com.typesafe.config.Config
6 | import de.innfactory.smithy4play
7 | import de.innfactory.smithy4play.{RouteResult, RoutingContext}
8 | import de.innfactory.smithy4play.middleware.MiddlewareBase
9 | import play.api.mvc.Results.NotFound
10 |
11 | import scala.concurrent.ExecutionContext.Implicits.global
12 | import scala.concurrent.Future
13 | import play.api.Logger
14 | import play.api.Mode.Prod
15 | import play.api.mvc._
16 | import play.api._
17 |
18 | class RouteBlacklistFilter @Inject() (config: Config, implicit val mat: Materializer, environment: Environment)
19 | extends MiddlewareBase {
20 |
21 | case class BlackListEntry(route: String, environment: Mode, method: String)
22 |
23 | private val accessLogger = Logger("AccessFilterLog")
24 |
25 | private val blacklistedRoutes = Seq[BlackListEntry]()
26 |
27 | /**
28 | * Check if route is contained in blacklistedRoutes and block request if true
29 | */
30 | override protected def skipMiddleware(r: RoutingContext): Boolean = {
31 | val path = r.requestHeader.path
32 | val method = r.requestHeader.method
33 | for (route <- blacklistedRoutes)
34 | if (environment.mode == route.environment && path.startsWith(route.route) && route.method == method)
35 | accessLogger.logger.warn(s"Illegal access to $path with $method in production")
36 | return true
37 | false
38 | }
39 |
40 | override protected def logic(
41 | r: RoutingContext,
42 | next: RoutingContext => RouteResult[smithy4play.EndpointResult]
43 | ): RouteResult[smithy4play.EndpointResult] =
44 | next.apply(r)
45 | }
46 |
--------------------------------------------------------------------------------
/.bin/firebase-data/auth_export/accounts.json:
--------------------------------------------------------------------------------
1 | {"kind":"identitytoolkit#DownloadAccountResponse","users":[
2 | {"localId":"GzHxC2T29yEl2UZXI3ZE1pc8kpQu","createdAt":"1620637847097","lastLoginAt":"1620637847097","passwordHash":"fakeHash:salt=fakeSaltEP7zIxRMDS8w8GiDi0kp:password=testtest","salt":"fakeSaltEP7zIxRMDS8w8GiDi0kp","passwordUpdatedAt":1620638292115,"providerUserInfo":[{"providerId":"password","email":"test2@test.de","federatedId":"ps.test4@innfactory.de","rawId":"test2@test.de","displayName":"","photoUrl":""}],"validSince":"1620638292","email":"test2@test.de","emailVerified":true,"disabled":false,"displayName":"","photoUrl":"","customAttributes":"{\n\"innFactoryAdmin\": true\n}","lastRefreshAt":"2021-05-10T09:22:42.172Z"},
3 | {"localId":"W88FqkZRc9TktJmu2Dow4xUkT0UU","createdAt":"1620638324553","lastLoginAt":"1620638324553","emailVerified":true,"email":"test@test.de","salt":"fakeSaltnv7MQIlUKtVXXmM5gTNQ","passwordHash":"fakeHash:salt=fakeSaltnv7MQIlUKtVXXmM5gTNQ:password=testtest","passwordUpdatedAt":1620638324553,"validSince":"1620638324","providerUserInfo":[{"providerId":"password","email":"test@test.de","federatedId":"test@test.de","rawId":"test@test.de","displayName":"Test test"}],"customAttributes":"{\"companyAdmin\":1}","displayName":"Test test"},
4 | {"localId":"WasFqkZRc9TktJmu2DoDSFienga2","createdAt":"1620638324553","lastLoginAt":"1620638324553","emailVerified":true,"email":"notverified@innfactory.de","salt":"fakeSaltnv7MQIlUKtVXXmM5gTNQ","passwordHash":"fakeHash:salt=fakeSaltnv7MQIlUKtVXXmM5gTNQ:password=testtest","passwordUpdatedAt":1620638324553,"validSince":"1620638324","providerUserInfo":[{"providerId":"password","email":"notverified@innfactory.de","federatedId":"notverified@innfactory.de","rawId":"notverified@innfactory.de","displayName":"Test test"}],"customAttributes":"{}","displayName":"Test test"}
5 | ]}
--------------------------------------------------------------------------------
/.bin/setup/src/main/scala/arguments/stringarg/StringArgValidations.scala:
--------------------------------------------------------------------------------
1 | package arguments.stringarg
2 |
3 | import cats.data.Validated
4 |
5 | object StringArgValidations {
6 | val cantBeEmpty: String => Either[String, Unit] = (toValidate: String) =>
7 | Validated.cond(toValidate.nonEmpty, (), "Can't be empty!").toEither
8 | val onlyLetters: String => Either[String, Unit] = (toValidate: String) =>
9 | Validated.cond(toValidate.matches("^[a-zA-Z]*?$"), (), "Can only consist out of letters!").toEither
10 | val onlyLettersNumbersHyphen: String => Either[String, Unit] = (toValidate: String) =>
11 | Validated
12 | .cond(
13 | toValidate.matches("^([a-zA-Z0-9]+(?:\\-?[a-zA-Z0-9]+))*?$"),
14 | (),
15 | "Can only consist out of letters and numbers separated by a single hyphen!"
16 | )
17 | .toEither
18 | val onlyLettersDot: String => Either[String, Unit] = (toValidate: String) =>
19 | Validated
20 | .cond(
21 | toValidate.matches("^([a-zA-Z]+(?:\\.[a-zA-Z]+))*?$"),
22 | (),
23 | "Can only consist out of letters separated by a single dot!"
24 | )
25 | .toEither
26 | val onlyLettersDotHyphen: String => Either[String, Unit] = (toValidate: String) =>
27 | Validated
28 | .cond(
29 | toValidate.matches("^([a-zA-Z]+(?:[\\.\\-]?[a-zA-Z]+))*?$"),
30 | (),
31 | "Can only consist out of letters separated by a single dot or hyphen!"
32 | )
33 | .toEither
34 | val onlyLettersDotSlash: String => Either[String, Unit] = (toValidate: String) =>
35 | Validated
36 | .cond(
37 | toValidate.matches("^([a-zA-Z]+(?:[\\.\\/]?[a-zA-Z]+))*?$"),
38 | (),
39 | "Can only consist out of letters separated by a single dot or slash!"
40 | )
41 | .toEither
42 | }
43 |
--------------------------------------------------------------------------------
/.bin/setup/src/main/scala/config/SetupConfig.scala:
--------------------------------------------------------------------------------
1 | package config
2 |
3 | import config.SetupConfig.{BootstrapConfig, ProjectConfig, SmithyConfig}
4 | import play.api.libs.json.{Json, OFormat}
5 |
6 | import java.nio.file.{Files, Path, Paths}
7 | import scala.io.Source
8 |
9 | case class SetupConfig(project: ProjectConfig, bootstrap: BootstrapConfig) {
10 | val smithy: SmithyConfig = SmithyConfig()
11 | }
12 |
13 | object SetupConfig {
14 | implicit val format: OFormat[SetupConfig] = Json.format[SetupConfig]
15 | val pathToProjectSetupConf = s"${System.getProperty("user.dir")}/.bin/"
16 | val fileName = "bootstrap-conf.json"
17 |
18 | def getFullPath(): Path = Paths.get(pathToProjectSetupConf + fileName)
19 |
20 | def get(): SetupConfig =
21 | if (Files.exists(getFullPath())) {
22 | val content = Files.readString(getFullPath())
23 | Json.parse(content).as[SetupConfig]
24 | } else {
25 | val defaultConfig = Source.fromResource(fileName).getLines().mkString(" ")
26 | Json.parse(defaultConfig).as[SetupConfig]
27 | }
28 |
29 | case class ProjectConfig(domain: String, name: String) {
30 | val sourcesRoot: String = "app"
31 | def getPackagePath() = s"$sourcesRoot/${domain.replace('.', '/')}"
32 | def getNamespace() = domain
33 | }
34 | object ProjectConfig {
35 | implicit val format: OFormat[ProjectConfig] = Json.format[ProjectConfig]
36 | }
37 | case class SmithyConfig(
38 | ) {
39 | val sourcesRoot: String = "modules/api-definition"
40 | val apiDefinitionRoot: String = "src.main.resources.META-INF.smithy"
41 | def getPath() = s"${sourcesRoot.replace('.', '/')}/${apiDefinitionRoot.replace('.', '/')}"
42 | }
43 | case class BootstrapConfig(paths: Seq[String])
44 | object BootstrapConfig {
45 | implicit val format: OFormat[BootstrapConfig] = Json.format[BootstrapConfig]
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/test/controllers/TestApplicationFactory.scala:
--------------------------------------------------------------------------------
1 | package controllers
2 |
3 | import akka.stream.Materializer
4 | import com.google.inject.Inject
5 | import org.scalatestplus.play.FakeApplicationFactory
6 | import play.api.inject.guice.GuiceApplicationBuilder
7 | import play.api.inject.{Binding, Module}
8 | import play.api.{Application, Configuration, Environment}
9 | import de.innfactory.play.flyway.test.TestFlywayMigrator
10 | import de.innfactory.smithy4play.client.{RequestClient, SmithyClientResponse}
11 | import io.opentelemetry.api.GlobalOpenTelemetry
12 | import play.api.mvc.AnyContentAsEmpty
13 | import play.api.test.FakeRequest
14 | import play.api.test.Helpers._
15 | import testutils.{AuthUtils, DateTimeUtil}
16 |
17 | import scala.concurrent.ExecutionContext
18 |
19 | /**
20 | * Set up an application factory that runs flyways migrations on in memory database.
21 | */
22 | trait TestApplicationFactory extends FakeApplicationFactory {
23 | implicit var ec: ExecutionContext = _
24 | implicit var mat: Materializer = _
25 | var authUtils: AuthUtils = _
26 |
27 | GlobalOpenTelemetry.resetForTest()
28 | DateTimeUtil.setToDateTime("2022-03-07T00:00:00.001Z")
29 |
30 | def fakeApplication(): Application = {
31 | val app = GuiceApplicationBuilder()
32 | .bindings(new FlywayModule)
33 | .build()
34 |
35 | ec = app.injector.instanceOf[ExecutionContext]
36 | mat = app.injector.instanceOf[Materializer]
37 | authUtils = app.injector.instanceOf[AuthUtils]
38 |
39 | app
40 | }
41 | }
42 |
43 | class FlywayModule extends Module {
44 | override def bindings(environment: Environment, configuration: Configuration): Seq[Binding[_]] =
45 | Seq(bind[FlywayMigrator].toSelf.eagerly())
46 | }
47 |
48 | class FlywayMigrator @Inject() (env: Environment, configuration: Configuration)
49 | extends TestFlywayMigrator(configuration, env)
50 |
--------------------------------------------------------------------------------
/app/de/innfactory/bootstrapplay2/actorsystem/application/ActorSystemHelloWorldController.scala:
--------------------------------------------------------------------------------
1 | package de.innfactory.bootstrapplay2.actorsystem.application
2 | import cats.data.EitherT
3 | import com.typesafe.config.Config
4 | import de.innfactory.bootstrapplay2.actorsystem.domain.commands.{ResponseQueryHelloWorld, ResponseQueryHelloWorldError}
5 | import de.innfactory.bootstrapplay2.actorsystem.domain.interfaces.HelloWorldService
6 | import de.innfactory.bootstrapplay2.api.{ActorSystemAPIController, HelloworldViaSystemResponse}
7 | import de.innfactory.smithy4play.{AutoRouting, ContextRoute}
8 | import play.api.Application
9 | import de.innfactory.bootstrapplay2.application.controller.BaseController
10 | import de.innfactory.play.results.errors.Errors.BadRequest
11 | import de.innfactory.play.smithy4play.ImplicitLogContext
12 | import play.api.mvc.ControllerComponents
13 |
14 | import javax.inject.{Inject, Singleton}
15 | import scala.concurrent.ExecutionContext
16 |
17 | @AutoRouting
18 | @Singleton
19 | class ActorSystemHelloWorldController @Inject() (
20 | helloWorldService: HelloWorldService
21 | )(implicit
22 | ec: ExecutionContext,
23 | cc: ControllerComponents,
24 | app: Application
25 | ) extends BaseController
26 | with ImplicitLogContext
27 | with ActorSystemAPIController[ContextRoute] {
28 |
29 | override def helloworldViaSystem(query: String): ContextRoute[HelloworldViaSystemResponse] =
30 | Endpoint
31 | .execute(_ =>
32 | EitherT {
33 | val result = for {
34 | response <- helloWorldService.queryHelloWorld(query)
35 | } yield response
36 | result.map {
37 | case ResponseQueryHelloWorld(_, answer) => Right(HelloworldViaSystemResponse(answer))
38 | case ResponseQueryHelloWorldError(_, error) => Left(BadRequest(error))
39 | }
40 | }
41 | )
42 | .complete
43 | }
44 |
--------------------------------------------------------------------------------
/app/de/innfactory/bootstrapplay2/filters/logging/AccessLoggingFilter.scala:
--------------------------------------------------------------------------------
1 | package de.innfactory.bootstrapplay2.filters.logging
2 |
3 | import javax.inject.Inject
4 | import akka.stream.Materializer
5 | import com.typesafe.config.Config
6 | import de.innfactory.smithy4play
7 | import de.innfactory.smithy4play.{ContextRouteError, EndpointResult, RouteResult, RoutingContext}
8 | import de.innfactory.smithy4play.middleware.MiddlewareBase
9 |
10 | import scala.concurrent.ExecutionContext.Implicits.global
11 | import scala.concurrent.Future
12 | import play.api.Logger
13 | import play.api.mvc._
14 | import play.api._
15 |
16 | class AccessLoggingFilter @Inject() (config: Config, implicit val mat: Materializer) extends MiddlewareBase {
17 | val accessLogger = Logger("AccessFilterLog")
18 |
19 | /**
20 | * status list from application.conf
21 | */
22 | private val configAccessStatus =
23 | config.getIntList("logging.access.statusList")
24 |
25 | /**
26 | * Logs requests if result header status is included in logging.access.statusList as defined in application.conf
27 | */
28 | override protected def logic(
29 | r: RoutingContext,
30 | next: RoutingContext => RouteResult[EndpointResult]
31 | ): RouteResult[EndpointResult] = {
32 | val request = r.requestHeader
33 | val result = next(r)
34 | result.leftMap { e =>
35 | if (shouldBeLogged(e)) {
36 | val msg =
37 | s"RequestID: status=${e.statusCode} method=${request.method} uri=${request.uri} remote-address=${request.remoteAddress}" +
38 | s" authorization-header=${request.headers.get("Authorization")}"
39 | accessLogger.warn(msg)
40 | }
41 | e
42 | }
43 | }
44 |
45 | /**
46 | * check if request/result should be logged
47 | */
48 | def shouldBeLogged(e: ContextRouteError): Boolean =
49 | configAccessStatus.contains(e.statusCode)
50 |
51 | }
52 |
--------------------------------------------------------------------------------
/app/de/innfactory/bootstrapplay2/users/infrastructure/mappers/UserRecordMapper.scala:
--------------------------------------------------------------------------------
1 | package de.innfactory.bootstrapplay2.users.infrastructure.mappers
2 |
3 | import com.google.firebase.auth.UserRecord
4 | import de.innfactory.bootstrapplay2.users.domain.models.{Claims, User, UserId}
5 | import play.api.Logger
6 | import play.api.libs.json.{JsValue, Json}
7 |
8 | import scala.collection.JavaConverters._
9 |
10 | object UserRecordMapper {
11 |
12 | def userRecordToUser(userRecord: UserRecord): User = {
13 | val claimsMap = userRecord.getCustomClaims.asScala.toMap.map(k => k._1 -> getValueFromAnyRef(k._2))
14 | User(
15 | userId = UserId(userRecord.getUid),
16 | email = userRecord.getEmail,
17 | emailVerified = userRecord.isEmailVerified,
18 | disabled = userRecord.isDisabled,
19 | claims = Claims(
20 | innFactoryAdmin = claimsMap.get("innFactoryAdmin").map(v => v.as[Boolean]),
21 | companyAdmin = claimsMap.get("companyAdmin").map(v => v.as[Long])
22 | ),
23 | firstName = None,
24 | lastName = None,
25 | displayName = userRecord.getDisplayName match {
26 | case s: String => Some(s)
27 | case null => None
28 | },
29 | lastSignIn = Some(userRecord.getUserMetadata.getLastSignInTimestamp),
30 | lastRefresh = Some(userRecord.getUserMetadata.getLastRefreshTimestamp),
31 | creation = Some(userRecord.getUserMetadata.getCreationTimestamp)
32 | )
33 | }
34 |
35 | def getValueFromAnyRef(a: AnyRef): JsValue =
36 | a match {
37 | case s: java.lang.String => Json.toJson(s)
38 | case l: java.math.BigDecimal => Json.toJson(l.longValue)
39 | case b: java.lang.Boolean => Json.toJson(b.booleanValue())
40 | case value =>
41 | Logger("play").logger.warn("Parsing unknown userClaims: " + value.getClass)
42 | Json.toJson("unknown")
43 | }
44 |
45 | }
46 |
--------------------------------------------------------------------------------
/test/resources/logback.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | %coloredLevel %logger{15} - %message%n%xException{10}
8 |
9 |
10 |
11 |
12 |
13 | 512
14 |
15 | 0
16 |
17 | false
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
--------------------------------------------------------------------------------
/.bin/setup/src/main/scala/packagedomain/DomainFile.scala:
--------------------------------------------------------------------------------
1 | package packagedomain
2 |
3 | import config.SetupConfig
4 |
5 | import java.nio.charset.StandardCharsets
6 | import java.nio.file.{Files, OpenOption, Path, Paths}
7 |
8 | trait DomainFile {
9 | def subPath: String
10 | def name: String
11 | def packageDomain: String
12 | def packageName: String
13 | protected def fileEnding: String
14 | protected def getContent(withCrud: Boolean)(implicit config: SetupConfig): String
15 |
16 | protected def getFileName(): String = s"$name.$fileEnding"
17 |
18 | def nameLowerCased(): String = Character.toLowerCase(name.charAt(0)) + name.substring(1)
19 |
20 | protected def getAdjustedSubPath(): String = if (subPath.nonEmpty)
21 | (subPath.head, subPath.last) match {
22 | case ('/', '/') => s"$subPath"
23 | case ('/', _) => s"$subPath/"
24 | case (_, '/') => s"/$subPath"
25 | case (_, _) => s"/$subPath/"
26 | }
27 | else "/"
28 |
29 | protected def getFullDirectoryPath()(implicit config: SetupConfig): String
30 |
31 | def writeDomainFile(withCrud: Boolean, openOptions: Option[OpenOption] = None)(implicit config: SetupConfig): Unit = {
32 | Files.createDirectories(Path.of(getFullDirectoryPath()))
33 | openOptions match {
34 | case Some(option) =>
35 | println(s"Writing ${getFileName()} into ${getFullDirectoryPath()} with option ${option.toString}")
36 | Files.write(
37 | Paths.get(getFullDirectoryPath() + getFileName()),
38 | getContent(withCrud).getBytes(StandardCharsets.UTF_8),
39 | option
40 | )
41 | case None =>
42 | println(s"Writing ${getFileName()} into ${getFullDirectoryPath()}")
43 | Files.write(
44 | Paths.get(getFullDirectoryPath() + getFileName()),
45 | getContent(withCrud).getBytes(StandardCharsets.UTF_8)
46 | )
47 | }
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/modules/slick/src/main/resources/logback.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | %coloredLevel %logger{15} - %message%n%xException{10}
8 |
9 |
10 |
11 |
12 |
13 | 512
14 |
15 | 0
16 |
17 | false
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
--------------------------------------------------------------------------------
/app/de/innfactory/bootstrapplay2/actorsharding/application/ActorShardingHelloWorldController.scala:
--------------------------------------------------------------------------------
1 | package de.innfactory.bootstrapplay2.actorsharding.application
2 |
3 | import cats.data.EitherT
4 | import com.typesafe.config.Config
5 | import de.innfactory.bootstrapplay2.actorsharding.domain.interfaces.HelloWorldService
6 | import de.innfactory.bootstrapplay2.actorsystem.domain.commands.{ResponseQueryHelloWorld, ResponseQueryHelloWorldError}
7 | import de.innfactory.bootstrapplay2.api.{ActorShardingAPIController, HelloworldViaShardingResponse}
8 | import de.innfactory.bootstrapplay2.application.controller.BaseController
9 | import de.innfactory.play.results.errors.Errors.BadRequest
10 | import de.innfactory.play.smithy4play.ImplicitLogContext
11 | import de.innfactory.smithy4play.{AutoRouting, ContextRoute}
12 | import play.api.Application
13 | import play.api.mvc.ControllerComponents
14 |
15 | import javax.inject.{Inject, Singleton}
16 | import scala.concurrent.ExecutionContext
17 |
18 | @AutoRouting
19 | @Singleton
20 | class ActorShardingHelloWorldController @Inject() (
21 | helloWorldService: HelloWorldService
22 | )(implicit ec: ExecutionContext, cc: ControllerComponents, app: Application, config: Config)
23 | extends BaseController
24 | with ImplicitLogContext
25 | with ActorShardingAPIController[ContextRoute] {
26 |
27 | override def helloworldViaSharding(query: String): ContextRoute[HelloworldViaShardingResponse] =
28 | Endpoint
29 | .execute(_ =>
30 | EitherT {
31 | val result = for {
32 | response <- helloWorldService.queryHelloWorld(query)
33 | } yield response
34 | result.map {
35 | case ResponseQueryHelloWorld(_, answer) => Right(HelloworldViaShardingResponse(answer))
36 | case ResponseQueryHelloWorldError(_, error) => Left(BadRequest(error))
37 | }
38 | }
39 | )
40 | .complete
41 | }
42 |
--------------------------------------------------------------------------------
/test/resources/migration/V999__DATA.sql:
--------------------------------------------------------------------------------
1 | INSERT INTO "company" ( "id",
2 | "settings",
3 | "string_attribute_1",
4 | "string_attribute_2",
5 | "long_attribute_1",
6 | "boolean_attribute",
7 | "created",
8 | "updated")
9 | VALUES (
10 | '0ce84627-9a66-46bf-9a1d-4f38b82a38e3',
11 | JSON '{"region": "region"}',
12 | 'test 1',
13 | 'test 2',
14 | 1,
15 | false,
16 | '2022-03-07T00:00:00.001Z',
17 | '2022-03-07T00:00:00.001Z'
18 | );
19 |
20 | INSERT INTO "company" ( "id",
21 | "settings",
22 | "string_attribute_1",
23 | "string_attribute_2",
24 | "long_attribute_1",
25 | "boolean_attribute",
26 | "created",
27 | "updated")
28 | VALUES (
29 | '7059f786-4633-4ace-a412-2f2e90556f08',
30 | JSON '{"region": "region"}',
31 | 'test 1',
32 | 'test 2',
33 | 1,
34 | false,
35 | '2022-03-07T00:00:00.001Z',
36 | '2022-03-07T00:00:00.001Z'
37 | );
38 |
39 | INSERT INTO "location" (
40 | "id",
41 | "company",
42 | "name",
43 | "settings",
44 | "address_line_1",
45 | "address_line_2",
46 | "zip",
47 | "city",
48 | "country",
49 | "created",
50 | "updated")
51 | VALUES (
52 | '592c5187-cb85-4b66-b0fc-293989923e1e',
53 | '0ce84627-9a66-46bf-9a1d-4f38b82a38e3',
54 | 'Location-1',
55 | JSON '{"location": "location"}',
56 | 'location_1_address_line_1',
57 | 'location_1_address_line_2',
58 | 'zip1',
59 | 'city1',
60 | 'country1',
61 | '2022-03-07T00:00:00.001Z',
62 | '2022-03-07T00:00:00.001Z'
63 | );
64 |
65 |
66 |
67 |
--------------------------------------------------------------------------------
/app/de/innfactory/bootstrapplay2/websockets/application/WebsocketController.scala:
--------------------------------------------------------------------------------
1 | package de.innfactory.bootstrapplay2.websockets.application
2 |
3 | import akka.Done
4 | import akka.actor.{ActorRef, ActorSystem}
5 | import akka.stream.Materializer
6 | import de.innfactory.bootstrapplay2.websockets.domain.interfaces.WebSocketService
7 | import play.api.libs.json.JsValue
8 |
9 | import akka.stream._
10 | import akka.stream.scaladsl.Source
11 | import de.innfactory.bootstrapplay2.websockets.infrastructure.actors.ScheduledActor
12 | import play.api.http.ContentTypes
13 | import play.api.libs.EventSource
14 | import javax.inject.{Inject, Singleton}
15 | import play.api.mvc.{WebSocket, _}
16 | import scala.concurrent.ExecutionContext
17 |
18 | @Singleton
19 | class WebsocketController @Inject() (
20 | cc: ControllerComponents,
21 | webSocketService: WebSocketService
22 | )(implicit ec: ExecutionContext, val system: ActorSystem, mat: Materializer)
23 | extends AbstractController(cc) {
24 |
25 | def socket =
26 | WebSocket.accept[JsValue, JsValue] { request =>
27 | webSocketService.socket
28 | }(WebSocket.MessageFlowTransformer.jsonMessageFlowTransformer)
29 |
30 | def serverSentEvents() = Action {
31 | val source: Source[Any, ActorRef] = Source.actorRef(
32 | completionMatcher = { case Done =>
33 | // complete stream immediately if we send it Done
34 | CompletionStrategy.immediately
35 | },
36 | // never fail the stream because of a message
37 | failureMatcher = PartialFunction.empty,
38 | bufferSize = 100,
39 | overflowStrategy = OverflowStrategy.dropHead
40 | )
41 |
42 | val pre = source.preMaterialize()
43 |
44 | val actorSource = pre._2.map {
45 | case msg: String => msg
46 | case _ => "Message"
47 | }
48 | system.actorOf(ScheduledActor.props(pre._1, 50, 250))
49 |
50 | val flow = actorSource via EventSource.flow
51 |
52 | flow.run()
53 |
54 | Ok.chunked(flow).as(ContentTypes.EVENT_STREAM)
55 |
56 | }
57 |
58 | }
59 |
--------------------------------------------------------------------------------
/app/de/innfactory/bootstrapplay2/locations/domain/services/DomainLocationService.scala:
--------------------------------------------------------------------------------
1 | package de.innfactory.bootstrapplay2.locations.domain.services
2 |
3 | import akka.NotUsed
4 | import akka.stream.scaladsl.Source
5 | import cats.data.EitherT
6 | import de.innfactory.bootstrapplay2.commons.RequestContextWithUser
7 | import de.innfactory.bootstrapplay2.companies.domain.models.CompanyId
8 | import de.innfactory.play.controller.ResultStatus
9 | import de.innfactory.bootstrapplay2.locations.domain.interfaces.{LocationRepository, LocationService}
10 | import de.innfactory.bootstrapplay2.locations.domain.models.{Location, LocationId}
11 |
12 | import javax.inject.Inject
13 | import scala.concurrent.{ExecutionContext, Future}
14 |
15 | private[locations] class DomainLocationService @Inject() (locationRepository: LocationRepository)(implicit
16 | ec: ExecutionContext
17 | ) extends LocationService {
18 |
19 | def getAllByCompany(companyId: CompanyId)(implicit
20 | rc: RequestContextWithUser
21 | ): EitherT[Future, ResultStatus, Seq[Location]] =
22 | locationRepository.getAllLocationsByCompany(companyId)
23 |
24 | def getAllAsStream()(implicit rc: RequestContextWithUser): EitherT[Future, ResultStatus, Source[Location, NotUsed]] =
25 | for {
26 | result <- EitherT.right[ResultStatus](Future(locationRepository.getAllLocationsAsSource))
27 | } yield result
28 |
29 | def getById(id: LocationId)(implicit rc: RequestContextWithUser): EitherT[Future, ResultStatus, Location] =
30 | locationRepository.getById(id)
31 |
32 | def updateLocation(company: Location)(implicit rc: RequestContextWithUser): EitherT[Future, ResultStatus, Location] =
33 | locationRepository.updateLocation(company)
34 |
35 | def createLocation(company: Location)(implicit rc: RequestContextWithUser): EitherT[Future, ResultStatus, Location] =
36 | locationRepository.createLocation(company)
37 |
38 | def deleteLocation(id: LocationId)(implicit rc: RequestContextWithUser): EitherT[Future, ResultStatus, Boolean] =
39 | locationRepository.deleteLocation(id)
40 | }
41 |
--------------------------------------------------------------------------------
/.bin/setup/src/main/scala/Setup.scala:
--------------------------------------------------------------------------------
1 | import packagedomain.PackageDomain
2 | import arguments.Args
3 | import bootstrap.Bootstrap
4 | import cats.data.Validated
5 | import config.SetupConfig
6 | import config.SetupConfig.{BootstrapConfig, ProjectConfig, SmithyConfig}
7 |
8 | import java.nio.file.{Files, Paths}
9 |
10 | object Setup extends App {
11 |
12 | implicit val config: SetupConfig = SetupConfig.get()
13 | val arguments = new Args(args)
14 |
15 | workingDirectoryIsProjectRoot() match {
16 | case Left(_) =>
17 | println(
18 | s"Execute this script from your project root! Your project root must contain the directories ${config.project.sourcesRoot} and ${config.smithy.sourcesRoot}"
19 | )
20 | case Right(_) =>
21 | if (args.isEmpty) {
22 | arguments.printHelp()
23 | }
24 | if (args.contains(Args.packageDomainKey)) {
25 | PackageDomain.create(
26 | arguments.packageDomain.packageName.toOption,
27 | arguments.packageDomain.domain.toOption,
28 | arguments.packageDomain.withCrud.toOption
29 | )
30 | }
31 | if (args.contains(Args.bootstrapKey)) {
32 | Bootstrap.init(
33 | SetupConfig(
34 | project = ProjectConfig(
35 | domain = arguments.bootstrap.projectDomain.toOption.getOrElse(""),
36 | name = arguments.bootstrap.projectName.toOption.getOrElse("")
37 | ),
38 | bootstrap = BootstrapConfig(
39 | paths = arguments.bootstrap.bootstrapPaths.toOption.getOrElse(Seq.empty)
40 | )
41 | )
42 | )
43 | }
44 | }
45 |
46 | def workingDirectoryIsProjectRoot()(implicit config: SetupConfig) =
47 | Validated
48 | .cond(
49 | Files.exists(Paths.get(System.getProperty("user.dir") + "/" + config.project.sourcesRoot.replace('.', '/'))) &&
50 | Files.exists(Paths.get(System.getProperty("user.dir") + "/" + config.smithy.sourcesRoot.replace('.', '/'))),
51 | (),
52 | ()
53 | )
54 | .toEither
55 | }
56 |
--------------------------------------------------------------------------------
/app/de/innfactory/bootstrapplay2/actorsharding/domain/services/HelloWorldServiceImpl.scala:
--------------------------------------------------------------------------------
1 | package de.innfactory.bootstrapplay2.actorsharding.domain.services
2 |
3 | import akka.actor._
4 | import akka.actor.typed.Scheduler
5 | import akka.cluster.sharding.typed.ShardingEnvelope
6 | import akka.cluster.sharding.typed.scaladsl.{ClusterSharding, Entity, EntityTypeKey}
7 | import akka.util.Timeout
8 | import de.innfactory.bootstrapplay2.actorsharding.domain.common.Sharding
9 | import de.innfactory.bootstrapplay2.actorsharding.domain.interfaces.HelloWorldService
10 | import de.innfactory.bootstrapplay2.actorsystem.domain.commands.{Command, QueryHelloWorld, Response}
11 | import akka.actor.typed.ActorRef
12 | import akka.actor.typed.scaladsl.AskPattern.Askable
13 | import de.innfactory.bootstrapplay2.actorsystem.domain.actors.HelloWorldActor
14 |
15 | import javax.inject._
16 | import scala.concurrent.duration._
17 | import scala.concurrent.{ExecutionContext, Future}
18 |
19 | @Singleton
20 | class HelloWorldServiceImpl @Inject() (
21 | )(implicit ec: ExecutionContext, system: ActorSystem, sharding: Sharding)
22 | extends HelloWorldService {
23 |
24 | implicit val timeout: Timeout = sharding.timeout
25 | implicit private val scheduler: Scheduler = sharding.getScheduler
26 | private val clusterShard: ClusterSharding = sharding.getSharding
27 |
28 | val helloWorldTag = "PLAN_CONVERSION"
29 | val helloWorldTypeKey: EntityTypeKey[Command] =
30 | EntityTypeKey[Command](helloWorldTag)
31 |
32 | val helloWorldShardRegion: ActorRef[ShardingEnvelope[Command]] =
33 | clusterShard.init(
34 | Entity(helloWorldTypeKey)(createBehavior = entityContext => {
35 | println(entityContext.entityId)
36 | HelloWorldActor()
37 | })
38 | )
39 |
40 | def queryHelloWorld(query: String): Future[Response] = {
41 | val result = helloWorldShardRegion.ask((ref: akka.actor.typed.ActorRef[Response]) =>
42 | ShardingEnvelope.apply(
43 | "shardingEnvelopeId",
44 | QueryHelloWorld(query, ref)
45 | )
46 | )
47 | result
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/app/de/innfactory/bootstrapplay2/companies/domain/services/DomainCompanyService.scala:
--------------------------------------------------------------------------------
1 | package de.innfactory.bootstrapplay2.companies.domain.services
2 |
3 | import akka.NotUsed
4 | import akka.stream.scaladsl.Source
5 | import cats.data.EitherT
6 | import de.innfactory.bootstrapplay2.commons.{RequestContext, RequestContextWithUser}
7 | import de.innfactory.play.controller.ResultStatus
8 | import de.innfactory.bootstrapplay2.companies.domain.interfaces.{CompanyRepository, CompanyService}
9 | import de.innfactory.bootstrapplay2.companies.domain.models.{Company, CompanyId}
10 |
11 | import javax.inject.Inject
12 | import scala.concurrent.{ExecutionContext, Future}
13 |
14 | private[companies] class DomainCompanyService @Inject() (companyRepository: CompanyRepository)(implicit
15 | ec: ExecutionContext
16 | ) extends CompanyService {
17 |
18 | def getAll()(implicit rc: RequestContextWithUser): EitherT[Future, ResultStatus, Seq[Company]] =
19 | companyRepository.getAll()
20 |
21 | def getAllForGraphQL(filterOptions: Option[String])(implicit rc: RequestContext): Future[Seq[Company]] =
22 | companyRepository.getAllForGraphQL(filterOptions)
23 |
24 | def getAllCompaniesAsStream()(implicit
25 | rc: RequestContextWithUser
26 | ): EitherT[Future, ResultStatus, Source[Company, NotUsed]] =
27 | for {
28 | result <- EitherT.right[ResultStatus](Future(companyRepository.getAllCompaniesAsSource))
29 | } yield result
30 |
31 | def getById(id: CompanyId)(implicit rc: RequestContextWithUser): EitherT[Future, ResultStatus, Company] =
32 | companyRepository.getById(id)
33 |
34 | def updateCompany(company: Company)(implicit rc: RequestContextWithUser): EitherT[Future, ResultStatus, Company] =
35 | companyRepository.updateCompany(company)
36 |
37 | def createCompany(company: Company)(implicit rc: RequestContextWithUser): EitherT[Future, ResultStatus, Company] =
38 | companyRepository.createCompany(company)
39 |
40 | def deleteCompany(id: CompanyId)(implicit rc: RequestContextWithUser): EitherT[Future, ResultStatus, Boolean] =
41 | companyRepository.deleteCompany(id)
42 | }
43 |
--------------------------------------------------------------------------------
/conf/logback.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | %coloredLevel %logger{15} - %message%n%xException{10}
8 |
9 |
10 |
11 |
12 |
13 | 512
14 |
15 | 0
16 |
17 | false
18 |
19 |
20 |
21 |
22 | conf/gcp-backend-sa.json
23 | de.innfactory.play.logging.logback.BaseEnhancer
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
--------------------------------------------------------------------------------
/conf/logback-local.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | %coloredLevel %logger{15} - %message%n%xException{10}
8 |
9 |
10 |
11 |
12 |
13 | 512
14 |
15 | 0
16 |
17 | false
18 |
19 |
20 |
21 |
22 | conf/gcp-backend-sa.json
23 | de.innfactory.play.logging.logback.BaseEnhancer
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
--------------------------------------------------------------------------------
/app/de/innfactory/bootstrapplay2/graphql/RequestExecutor.scala:
--------------------------------------------------------------------------------
1 | package de.innfactory.bootstrapplay2.graphql
2 |
3 | import com.typesafe.config.Config
4 | import de.innfactory.bootstrapplay2.commons.RequestContext
5 | import de.innfactory.bootstrapplay2.graphql.schema.SchemaDefinition
6 | import de.innfactory.grapqhl.play.request.RequestExecutionBase
7 | import de.innfactory.play.smithy4play.HttpHeaders
8 | import de.innfactory.play.tracing.{OpentelemetryProvider, TracingHelper}
9 | import io.opentelemetry.api.trace.Span
10 | import play.api.mvc.{AnyContent, Request}
11 |
12 | import javax.inject.Inject
13 | import scala.concurrent.{ExecutionContext, Future}
14 |
15 | class RequestExecutor @Inject() (implicit config: Config)
16 | extends RequestExecutionBase[GraphQLExecutionContext, ExecutionServices](SchemaDefinition.graphQLSchema) {
17 | override def contextBuilder(services: ExecutionServices, request: Request[AnyContent])(implicit
18 | ec: ExecutionContext
19 | ): GraphQLExecutionContext =
20 | GraphQLExecutionContext(
21 | request = request,
22 | ec = ec,
23 | companiesService = services.companiesService,
24 | locationsService = services.locationsService
25 | )
26 | }
27 |
28 | object RequestExecutor {
29 | implicit class EnhancedRequest(request: Request[AnyContent]) {
30 | def toRequestContextAndExecute[T](spanString: String, f: RequestContext => Future[T])(implicit
31 | ec: ExecutionContext
32 | ): Future[T] = {
33 | val parentSpan = TracingHelper.generateSpanFromRemoteSpan(HttpHeaders(request.headers.toMap))
34 | val processRequest = (child: Span, parent: Option[Span]) => {
35 | val rc = new RequestContext(HttpHeaders(request.headers.toMap), Some(child))
36 | val result = f(rc)
37 | result.map { r =>
38 | parent.foreach(_.end())
39 | r
40 | }
41 | }
42 | parentSpan match {
43 | case Some(parent) =>
44 | TracingHelper.traceWithParent(spanString, parent, child => processRequest(child, parentSpan))
45 | case None =>
46 | val child = OpentelemetryProvider.getTracer.spanBuilder(spanString).startSpan()
47 | processRequest(child, parentSpan)
48 | }
49 | }
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/app/de/innfactory/bootstrapplay2/users/infrastructure/SlickUserResetTokenRepository.scala:
--------------------------------------------------------------------------------
1 | package de.innfactory.bootstrapplay2.users.infrastructure
2 |
3 | import cats.data.EitherT
4 | import cats.implicits.catsSyntaxEitherId
5 | import com.typesafe.config.Config
6 | import dbdata.Tables
7 | import de.innfactory.play.db.codegen.XPostgresProfile.api._
8 | import de.innfactory.play.controller.ResultStatus
9 | import de.innfactory.bootstrapplay2.commons.infrastructure.BaseSlickRepository
10 | import de.innfactory.bootstrapplay2.users.domain.interfaces.UserPasswordResetTokenRepository
11 | import de.innfactory.bootstrapplay2.users.domain.models.{UserId, UserPasswordResetToken}
12 | import de.innfactory.bootstrapplay2.users.infrastructure.mappers.UserPasswordResetTokenMapper._
13 | import de.innfactory.play.tracing.TraceContext
14 | import play.api.inject.ApplicationLifecycle
15 |
16 | import javax.inject.Inject
17 | import slick.jdbc.JdbcBackend.Database
18 |
19 | import scala.concurrent.{ExecutionContext, Future}
20 | import scala.language.implicitConversions
21 |
22 | class SlickUserResetTokenRepository @Inject() (db: Database, lifecycle: ApplicationLifecycle)(implicit
23 | ec: ExecutionContext,
24 | config: Config
25 | ) extends BaseSlickRepository(db)
26 | with UserPasswordResetTokenRepository {
27 |
28 | def getForUser(userId: UserId)(implicit rc: TraceContext): EitherT[Future, ResultStatus, UserPasswordResetToken] =
29 | lookupGeneric(
30 | Tables.UserPasswordResetTokens.filter(_.userId === userId.value).result.headOption
31 | )
32 |
33 | def create(
34 | entity: UserPasswordResetToken
35 | )(implicit rc: TraceContext): EitherT[Future, ResultStatus, UserPasswordResetToken] =
36 | EitherT(
37 | db.run(
38 | Tables.UserPasswordResetTokens insertOrUpdate entity
39 | ).map(_.asRight[ResultStatus].map(_ => entity))
40 | )
41 |
42 | def delete(entity: UserPasswordResetToken)(implicit rc: TraceContext): EitherT[Future, ResultStatus, Int] =
43 | EitherT(
44 | db.run(Tables.UserPasswordResetTokens.filter(_.userId === entity.userId.value).delete)
45 | .map(_.asRight[ResultStatus])
46 | )
47 |
48 | lifecycle.addStopHook(() =>
49 | Future.successful {
50 | close()
51 | }
52 | )
53 | }
54 |
--------------------------------------------------------------------------------
/app/de/innfactory/bootstrapplay2/application/controller/BaseController.scala:
--------------------------------------------------------------------------------
1 | package de.innfactory.bootstrapplay2.application.controller
2 |
3 | import cats.data.{EitherT, Kleisli}
4 | import de.innfactory.bootstrapplay2.commons.{RequestContext, RequestContextWithUser}
5 | import de.innfactory.bootstrapplay2.commons.application.actions.utils.UserUtils
6 | import de.innfactory.bootstrapplay2.users.domain.models.User
7 | import de.innfactory.play.controller.{ErrorResult, ResultStatus}
8 | import de.innfactory.smithy4play.{ContextRouteError, RoutingContext}
9 | import play.api.Application
10 | import de.innfactory.play.smithy4play.{AbstractBaseController, HttpHeaders}
11 | import de.innfactory.play.smithy4play.ImplicitLogContext
12 | import play.api.libs.json.{JsValue, Json}
13 |
14 | import scala.concurrent.ExecutionContext
15 |
16 | class BaseController(implicit ec: ExecutionContext, app: Application)
17 | extends AbstractBaseController[ResultStatus, RequestContextWithUser, RequestContext]
18 | with ImplicitLogContext
19 | with BaseMapper {
20 |
21 | private val userUtils = app.injector.instanceOf[UserUtils[User]]
22 |
23 | override def AuthAction: Kleisli[ApplicationRouteResult, RequestContext, RequestContextWithUser] = Kleisli {
24 | context =>
25 | val result = for {
26 | _ <- EitherT(userUtils.validateJwtToken(context.httpHeaders.authAsJwt))
27 | user <- EitherT(userUtils.getUser(context.httpHeaders.authAsJwt))
28 | } yield RequestContextWithUser(context.httpHeaders, context.span, user)
29 | result
30 | }
31 |
32 | case class ErrorJson(message: String, additionalInfoErrorCode: Option[String])
33 |
34 | private val writes = Json.writes[ErrorJson]
35 |
36 | override def errorHandler(e: ResultStatus): ContextRouteError =
37 | e match {
38 | case result: ErrorResult =>
39 | new ContextRouteError {
40 | override def message: String = result.message
41 | override def statusCode: Int = result.statusCode
42 | override def toJson: JsValue = Json.toJson(ErrorJson(result.message, result.additionalInfoErrorCode))(writes)
43 | }
44 | }
45 |
46 | override def createRequestContextFromRoutingContext(r: RoutingContext): RequestContext =
47 | new RequestContext(HttpHeaders(r.headers))
48 | }
49 |
--------------------------------------------------------------------------------
/.bin/setup/src/main/scala/arguments/Args.scala:
--------------------------------------------------------------------------------
1 | package arguments
2 |
3 | import arguments.booleanarg.BooleanArgValidations
4 | import arguments.stringarg.StringArgValidations
5 | import org.rogach.scallop.{ScallopConf, ScallopOption, Subcommand}
6 |
7 | class Args(arguments: Seq[String]) extends ScallopConf(arguments) {
8 | object packageDomain extends Subcommand(Args.packageDomainKey) {
9 | val packageName: ScallopOption[String] = opt[String](
10 | descr = "Name of the package e.g. companies",
11 | validate = (input: String) => StringArgValidations.onlyLetters(input).isRight
12 | )
13 | val domain: ScallopOption[String] = opt[String](
14 | descr = "Name of the domain e.g. company",
15 | validate = (input: String) => StringArgValidations.onlyLettersDot(input).isRight
16 | )
17 | val withCrud: ScallopOption[String] =
18 | opt[String](
19 | name = "crud",
20 | short = 'c',
21 | descr = "(y/n) generates code for crud operations",
22 | validate = (booleanString: String) => BooleanArgValidations.isBooleanString(booleanString).isRight
23 | )
24 | }
25 |
26 | object bootstrap extends Subcommand(Args.bootstrapKey) {
27 | val projectName: ScallopOption[String] =
28 | opt[String](
29 | descr = "Name of the project e.g. bootstrap-play2",
30 | validate = (input: String) => StringArgValidations.onlyLettersNumbersHyphen(input).isRight
31 | )
32 | val projectDomain: ScallopOption[String] =
33 | opt[String](
34 | descr = "Folder name of the projects packages, e.g. de.innfactory.bootstrapplay2",
35 | validate = (input: String) => StringArgValidations.onlyLettersDot(input).isRight
36 | )
37 | val bootstrapPaths: ScallopOption[List[String]] = opt[List[String]](
38 | descr =
39 | "Paths of files and directories which shall be included during the bootstrap process, sources roots and build.sbt are always included",
40 | validate =
41 | (inputs: List[String]) => inputs.forall(input => StringArgValidations.onlyLettersDotSlash(input).isRight)
42 | )
43 | }
44 | addSubcommand(packageDomain)
45 | addSubcommand(bootstrap)
46 | verify()
47 | }
48 |
49 | object Args {
50 | val packageDomainKey = "package"
51 | val bootstrapKey = "bootstrap"
52 | }
53 |
--------------------------------------------------------------------------------
/app/de/innfactory/bootstrapplay2/commons/errors/ErrorHandler.scala:
--------------------------------------------------------------------------------
1 | package de.innfactory.bootstrapplay2.commons.errors
2 |
3 | import de.innfactory.bootstrapplay2.commons.results.ErrorResponseWithAdditionalBody
4 | import de.innfactory.play.controller.ErrorResponse
5 | import play.api.http.HttpErrorHandler
6 | import play.api.mvc._
7 | import play.api.mvc.Results._
8 |
9 | import scala.concurrent._
10 | import javax.inject.Singleton
11 | import play.api.Logger
12 |
13 | @Singleton
14 | class ErrorHandler extends HttpErrorHandler {
15 |
16 | /**
17 | * Custom error handler for custom client error handling
18 | * @param request
19 | * @param statusCode
20 | * @param message
21 | * @return
22 | */
23 | def onClientError(request: RequestHeader, statusCode: Int, message: String) =
24 | Future.successful(
25 | Status(statusCode)("A client error occurred")
26 | )
27 |
28 | case class JsonError(jsonPath: String, errorDetails: String)
29 |
30 | import play.api.libs.json.Json
31 | object JsonError {
32 | implicit val format = Json.format[JsonError]
33 | }
34 |
35 | /**
36 | * Custom error handler for custom server error handling
37 | * @param request
38 | * @param exception
39 | * @return
40 | */
41 | def onServerError(request: RequestHeader, exception: Throwable) = {
42 | val log = Logger("play")
43 | Future.successful(exception match {
44 | case e: play.api.libs.json.JsResultException =>
45 | BadRequest(
46 | ErrorResponseWithAdditionalBody(
47 | "Invalid Json",
48 | Json.toJson(
49 | e.errors.map(error =>
50 | JsonError(
51 | error._1.path.mkString(", "),
52 | error._2.flatMap(_.messages).mkString(", ")
53 | )
54 | )
55 | )
56 | ).toJson
57 | )
58 | case pg: org.postgresql.util.PSQLException =>
59 | log.error(request.toString(), exception)
60 | BadRequest(ErrorResponse("PSQL Exception, maybe duplicate key or foreign key constraint").toJson)
61 | case _ =>
62 | println(exception)
63 | println(request)
64 | log.error(request.toString(), exception)
65 | InternalServerError(ErrorResponse("unknown internal server error").toJson)
66 | })
67 |
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/app/de/innfactory/bootstrapplay2/companies/application/CompanyController.scala:
--------------------------------------------------------------------------------
1 | package de.innfactory.bootstrapplay2.companies.application
2 |
3 | import com.typesafe.config.Config
4 | import de.innfactory.bootstrapplay2.application.controller.BaseController
5 | import de.innfactory.bootstrapplay2.companies.application.mapper.CompanyMapper
6 | import de.innfactory.bootstrapplay2.companies.domain.interfaces.CompanyService
7 | import play.api.mvc.ControllerComponents
8 | import de.innfactory.bootstrapplay2.companies.domain.models.CompanyId
9 | import de.innfactory.bootstrapplay2.api.{CompaniesResponse, CompanyAPIController, CompanyRequestBody, CompanyResponse}
10 | import de.innfactory.play.smithy4play.ImplicitLogContext
11 | import de.innfactory.smithy4play.{AutoRouting, ContextRoute}
12 | import play.api.Application
13 |
14 | import javax.inject.{Inject, Singleton}
15 | import scala.concurrent.ExecutionContext
16 | import scala.language.implicitConversions
17 |
18 | @AutoRouting
19 | @Singleton
20 | class CompanyController @Inject() (
21 | companyService: CompanyService
22 | )(implicit
23 | ec: ExecutionContext,
24 | cc: ControllerComponents,
25 | app: Application,
26 | config: Config
27 | ) extends BaseController
28 | with ImplicitLogContext
29 | with CompanyAPIController[ContextRoute]
30 | with CompanyMapper {
31 |
32 | override def getCompanyById(companyId: de.innfactory.bootstrapplay2.api.CompanyId): ContextRoute[CompanyResponse] =
33 | Endpoint.withAuth
34 | .execute(companyService.getById(companyId)(_))
35 | .complete
36 |
37 | override def getAllCompanies(): ContextRoute[CompaniesResponse] =
38 | Endpoint.withAuth
39 | .execute(companyService.getAll()(_))
40 | .complete
41 |
42 | override def createCompany(body: CompanyRequestBody): ContextRoute[CompanyResponse] = Endpoint.withAuth
43 | .execute(companyService.createCompany(body)(_))
44 | .complete
45 |
46 | override def updateCompany(body: CompanyRequestBody): ContextRoute[CompanyResponse] = Endpoint.withAuth
47 | .execute(companyService.updateCompany(body)(_))
48 | .complete
49 |
50 | override def deleteCompany(companyId: de.innfactory.bootstrapplay2.api.CompanyId): ContextRoute[Unit] =
51 | Endpoint.withAuth
52 | .execute(companyService.deleteCompany(companyId)(_))
53 | .complete
54 |
55 | }
56 |
--------------------------------------------------------------------------------
/app/de/innfactory/bootstrapplay2/users/domain/interfaces/UserService.scala:
--------------------------------------------------------------------------------
1 | package de.innfactory.bootstrapplay2.users.domain.interfaces
2 |
3 | import akka.NotUsed
4 | import akka.stream.scaladsl.Source
5 | import cats.data.EitherT
6 | import com.google.inject.ImplementedBy
7 | import de.innfactory.bootstrapplay2.commons.RequestContextWithUser
8 | import de.innfactory.bootstrapplay2.users.domain.models.{Claims, User, UserId}
9 | import de.innfactory.bootstrapplay2.users.domain.services.DomainUserService
10 | import de.innfactory.play.controller.ResultStatus
11 | import de.innfactory.play.tracing.TraceContext
12 |
13 | import scala.concurrent.Future
14 |
15 | @ImplementedBy(classOf[DomainUserService])
16 | trait UserService {
17 |
18 | def sendPasswordResetToken()(implicit rc: RequestContextWithUser): EitherT[Future, ResultStatus, Unit]
19 |
20 | def getAllUsersAsSource(implicit rc: RequestContextWithUser): Source[User, NotUsed]
21 |
22 | def getUserByEmail(email: String)(implicit rc: RequestContextWithUser): EitherT[Future, ResultStatus, User]
23 |
24 | def getUserById(userId: UserId)(rc: RequestContextWithUser): EitherT[Future, ResultStatus, User]
25 |
26 | def getUserByIdWithoutRequestContext(userId: UserId): EitherT[Future, ResultStatus, User]
27 |
28 | def createUser(email: String)(implicit rc: RequestContextWithUser): EitherT[Future, ResultStatus, User]
29 |
30 | def upsertUser(user: User, oldUser: User)(implicit rc: RequestContextWithUser): EitherT[Future, ResultStatus, User]
31 |
32 | def setUserClaims(userId: UserId, claims: Claims)(implicit
33 | rc: RequestContextWithUser
34 | ): EitherT[Future, ResultStatus, Boolean]
35 |
36 | def setUserPassword(userId: UserId, newPassword: String)(implicit
37 | rc: TraceContext
38 | ): EitherT[Future, ResultStatus, User]
39 |
40 | def setEmailVerifiedState(userId: UserId, state: Boolean)(implicit
41 | rc: RequestContextWithUser
42 | ): EitherT[Future, ResultStatus, Boolean]
43 |
44 | def setUserDisabledState(userId: UserId, state: Boolean)(implicit
45 | rc: RequestContextWithUser
46 | ): EitherT[Future, ResultStatus, Boolean]
47 |
48 | def setUserDisplayName(userId: UserId, displayName: String)(implicit
49 | rc: RequestContextWithUser
50 | ): EitherT[Future, ResultStatus, Boolean]
51 |
52 | def setEmailVerified(userId: UserId)(implicit rc: RequestContextWithUser): EitherT[Future, ResultStatus, Boolean]
53 |
54 | }
55 |
--------------------------------------------------------------------------------
/.bin/setup/src/main/scala/packagedomain/scalafiles/SlickMapper.scala:
--------------------------------------------------------------------------------
1 | package packagedomain.scalafiles
2 |
3 | import packagedomain.common.CrudHelper
4 | import config.SetupConfig
5 |
6 | case class SlickMapper(packageDomain: String, packageName: String) extends ScalaDomainFile with CrudHelper {
7 | override def subPath =
8 | s"/$packageName/infrastructure/mapper/"
9 | val name = s"${packageDomain.capitalize}Mapper"
10 | override def getContent(withCrud: Boolean)(implicit config: SetupConfig): String = {
11 | val domainModel = DomainModel(packageDomain, packageName)
12 | val domainModelId = DomainModelId(packageDomain, packageName)
13 | val content = s"""
14 | |package ${config.project.getNamespace()}.$packageName.infrastructure.mapper
15 | |
16 | |import dbdata.Tables
17 | |${CrudImportsKey}
18 | |
19 | |private[infrastructure] object $name {
20 | | ${CrudLogicKey}
21 | |}
22 | |""".stripMargin
23 |
24 | replaceForCrud(
25 | content,
26 | withCrud,
27 | createCrudLogic(domainModel, domainModelId),
28 | createCrudImports(domainModel, domainModelId)
29 | )
30 | }
31 |
32 | private def createCrudImports(domainModel: DomainModel, domainModelId: DomainModelId)(implicit
33 | config: SetupConfig
34 | ): String =
35 | s"""
36 | |import ${config.project.getNamespace()}.$packageName.domain.models.{${domainModel.name}, ${domainModelId.name}}
37 | |import io.scalaland.chimney.dsl.TransformerOps
38 | |""".stripMargin
39 |
40 | private def createCrudLogic(domainModel: DomainModel, domainModelId: DomainModelId): String =
41 | s"""
42 | | implicit def ${domainModel
43 | .nameLowerCased()}RowTo${domainModel.name}(row: Tables.${domainModel.name}Row): ${domainModel.name} =
44 | | row
45 | | .into[${domainModel.name}]
46 | | .withFieldComputed(_.id, r => ${domainModelId.name}(r.id))
47 | | .transform
48 | |
49 | | implicit def ${domainModel.nameLowerCased()}To${domainModel.name}Row(${domainModel
50 | .nameLowerCased()}: ${domainModel.name}): Tables.${domainModel.name}Row =
51 | | ${domainModel.nameLowerCased()}
52 | | .into[Tables.${domainModel.name}Row]
53 | | .withFieldComputed[String, String](_.id, c => c.id.value)
54 | | .transform
55 | |""".stripMargin
56 | }
57 |
--------------------------------------------------------------------------------
/.bin/setup/src/main/scala/packagedomain/PackageDomain.scala:
--------------------------------------------------------------------------------
1 | package packagedomain
2 |
3 | import packagedomain.scalafiles.{
4 | ApplicationController,
5 | ApplicationMapper,
6 | DomainModel,
7 | DomainModelId,
8 | DomainService,
9 | Repository,
10 | Service,
11 | SlickMapper,
12 | SlickRepository
13 | }
14 | import packagedomain.smithyfiles.{ApiDefinition, ApiManifest}
15 | import arguments.booleanarg.{BooleanArgRetriever, BooleanArgValidations}
16 | import arguments.stringarg.StringArgRetriever
17 | import config.SetupConfig
18 |
19 | import java.nio.file.StandardOpenOption
20 | import scala.sys.process.Process
21 |
22 | object PackageDomain {
23 | def create(packageNameArg: Option[String], packageDomainArg: Option[String], withCrudArg: Option[String])(implicit
24 | config: SetupConfig
25 | ): Unit = {
26 | val packageName = packageNameArg.getOrElse(StringArgRetriever.askFor("Package name, e.g. companies"))
27 | val packageDomain = packageDomainArg.getOrElse(StringArgRetriever.askFor("Package domain, e.g. company"))
28 | val withCrud = (withCrudArg match {
29 | case Some(value) =>
30 | BooleanArgValidations.isBooleanString(value) match {
31 | case Left(_) => None
32 | case Right(boolean) => Some(boolean)
33 | }
34 | case None => None
35 | }).getOrElse(BooleanArgRetriever.askFor("With crud (create, read, update, delete)?"))
36 |
37 | println(s"Writing package into ${System.getProperty("user.dir")}/")
38 |
39 | // package
40 | ApplicationController(packageDomain, packageName).writeDomainFile(withCrud)
41 | ApplicationMapper(packageDomain, packageName).writeDomainFile(withCrud)
42 | Repository(packageDomain, packageName).writeDomainFile(withCrud)
43 | Service(packageDomain, packageName).writeDomainFile(withCrud)
44 | DomainModel(packageDomain, packageName).writeDomainFile(withCrud)
45 | DomainModelId(packageDomain, packageName).writeDomainFile(withCrud)
46 | DomainService(packageDomain, packageName).writeDomainFile(withCrud)
47 | SlickRepository(packageDomain, packageName).writeDomainFile(withCrud)
48 | SlickMapper(packageDomain, packageName).writeDomainFile(withCrud)
49 |
50 | // smithy
51 | ApiDefinition(packageDomain, packageName).writeDomainFile(withCrud)
52 | ApiManifest(packageDomain, packageName).writeDomainFile(withCrud, Some(StandardOpenOption.APPEND))
53 | println(s"Done writing, compiling code...")
54 | Process("sbt compile").run()
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/app/de/innfactory/bootstrapplay2/users/infrastructure/UserRepositoryMock.scala:
--------------------------------------------------------------------------------
1 | package de.innfactory.bootstrapplay2.users.infrastructure
2 |
3 | import akka.NotUsed
4 | import akka.stream.scaladsl.Source
5 | import cats.data.EitherT
6 | import cats.implicits.catsSyntaxEitherId
7 | import de.innfactory.bootstrapplay2.users.domain.interfaces.UserRepository
8 | import de.innfactory.bootstrapplay2.users.domain.models.{Claims, User, UserId}
9 | import de.innfactory.play.controller.ResultStatus
10 |
11 | import javax.inject.Inject
12 | import scala.concurrent.{ExecutionContext, Future}
13 |
14 | class UserRepositoryMock @Inject() (implicit ec: ExecutionContext) extends UserRepository {
15 |
16 | private def defaultUser(userId: UserId = UserId("userId")) =
17 | User(
18 | userId = userId,
19 | email = "email",
20 | emailVerified = true,
21 | disabled = false,
22 | claims = Claims(),
23 | firstName = None,
24 | lastName = None,
25 | displayName = Some("defaultUser"),
26 | lastSignIn = None,
27 | lastRefresh = None,
28 | creation = None
29 | )
30 |
31 | def getAllUsersAsSource: Source[User, NotUsed] = Source.apply(Seq.empty[User])
32 |
33 | def getUserByEmail(email: String): EitherT[Future, ResultStatus, User] =
34 | EitherT.rightT(defaultUser())
35 |
36 | def getUserById(userId: UserId): EitherT[Future, ResultStatus, User] = EitherT.rightT(
37 | defaultUser(userId)
38 | )
39 |
40 | def createUser(email: String): EitherT[Future, ResultStatus, User] =
41 | EitherT.rightT(defaultUser())
42 |
43 | def upsertUser(user: User, oldUser: User): EitherT[Future, ResultStatus, User] = EitherT.rightT(
44 | defaultUser()
45 | )
46 |
47 | def setUserClaims(userId: UserId, claims: Claims): EitherT[Future, ResultStatus, Boolean] = EitherT.rightT(
48 | true
49 | )
50 |
51 | def setUserPassword(userId: UserId, newPassword: String): EitherT[Future, ResultStatus, User] = EitherT.rightT(
52 | defaultUser()
53 | )
54 |
55 | def setEmailVerifiedState(userId: UserId, state: Boolean): EitherT[Future, ResultStatus, Boolean] = EitherT.rightT(
56 | true
57 | )
58 |
59 | def setUserDisabledState(userId: UserId, state: Boolean): EitherT[Future, ResultStatus, Boolean] = EitherT.rightT(
60 | true
61 | )
62 |
63 | def setUserDisplayName(userId: UserId, displayName: String): EitherT[Future, ResultStatus, Boolean] = EitherT.rightT(
64 | true
65 | )
66 |
67 | def setEmailVerified(userId: UserId): EitherT[Future, ResultStatus, Boolean] =
68 | EitherT.rightT(true)
69 | }
70 |
--------------------------------------------------------------------------------
/test/controllers/ActorControllerTest.scala:
--------------------------------------------------------------------------------
1 | package controllers
2 |
3 | import de.innfactory.bootstrapplay2.api.{ActorShardingAPIControllerGen, ActorSystemAPIControllerGen}
4 | import de.innfactory.smithy4play.client.GenericAPIClient.EnhancedGenericAPIClient
5 |
6 | import org.scalatestplus.play.{BaseOneAppPerSuite, PlaySpec}
7 | import testutils.FakeRequestClient
8 | import de.innfactory.smithy4play.client.SmithyPlayTestUtils._
9 |
10 | import java.nio.charset.StandardCharsets
11 |
12 | class ActorControllerTest extends PlaySpec with BaseOneAppPerSuite with TestApplicationFactory {
13 | private val actorSystemClient =
14 | ActorSystemAPIControllerGen.withClientAndHeaders(new FakeRequestClient(), Some(Map("Authorization" -> Seq("key"))))
15 | private val actorShardingClient =
16 | ActorShardingAPIControllerGen.withClientAndHeaders(
17 | new FakeRequestClient(),
18 | Some(Map("Authorization" -> Seq("key")))
19 | )
20 |
21 | /** —————————————————————— */
22 | /** ACTORSCONTROLLER */
23 | /** —————————————————————— */
24 | "ActorSystem" must {
25 | "query invalid message" in {
26 | val result = actorSystemClient.helloworldViaSystem("test").awaitLeft
27 | result.statusCode mustBe 400
28 | val error = new String(result.error, StandardCharsets.UTF_8)
29 | error mustBe "{\"message\":\"the query was not 'hello'\"}"
30 | }
31 |
32 | "query hello" in {
33 | val result = actorSystemClient.helloworldViaSystem("hello").awaitRight
34 | result.statusCode mustBe result.expectedStatusCode
35 | }
36 |
37 | "throughput" in {
38 | for (_ <- 0 to 10) {
39 | val result = actorSystemClient.helloworldViaSystem("hello").awaitRight
40 | result.statusCode mustBe result.expectedStatusCode
41 | }
42 | }
43 | }
44 |
45 | "ActorSharding" must {
46 | "query invalid message" in {
47 | val result = actorShardingClient.helloworldViaSharding("test").awaitLeft
48 | result.statusCode mustBe 400
49 | val error = new String(result.error, StandardCharsets.UTF_8)
50 | error mustBe "{\"message\":\"the query was not 'hello'\"}"
51 | }
52 |
53 | "query hello" in {
54 | val result = actorShardingClient.helloworldViaSharding("hello").awaitRight
55 | result.statusCode mustBe result.expectedStatusCode
56 | }
57 |
58 | "throughput" in {
59 | for (_ <- 0 to 10) {
60 | val result = actorShardingClient.helloworldViaSharding("hello").awaitRight
61 | result.statusCode mustBe result.expectedStatusCode
62 | }
63 | }
64 | }
65 |
66 | }
67 |
--------------------------------------------------------------------------------
/app/de/innfactory/bootstrapplay2/application/keycloak/infrastructure/KeycloakOAuthRepository.scala:
--------------------------------------------------------------------------------
1 | package de.innfactory.bootstrapplay2.application.keycloak.infrastructure
2 |
3 | import com.typesafe.config.Config
4 | import play.api.cache.SyncCacheApi
5 | import play.api.libs.json.{Json, OFormat}
6 | import play.api.libs.ws.WSClient
7 | import play.api.libs.ws.ahc.AhcWSResponse
8 |
9 | import javax.inject.Inject
10 | import scala.concurrent.duration.DurationInt
11 | import scala.concurrent.{ExecutionContext, Future}
12 |
13 | class KeycloakOAuthRepository @Inject() (wsClient: WSClient, syncCacheApi: SyncCacheApi, config: Config)(implicit
14 | ec: ExecutionContext
15 | ) {
16 | class InvalidKeyCloakAuth(message: String) extends Throwable
17 |
18 | case class Token(
19 | access_token: String,
20 | expires_in: Int
21 | )
22 |
23 | object Token {
24 | implicit val format: OFormat[Token] = Json.format[Token]
25 | }
26 |
27 | private val URL = config.getString("keycloak.url")
28 | private val BASE_PATH = config.getString("keycloak.basePath")
29 | private val CLIENT_ID = config.getString("keycloak.clientId")
30 | private val CLIENT_SECRET = config.getString("keycloak.clientSecret")
31 | private val REALM = config.getString("keycloak.realm")
32 | private val AUTH_REALM = config.getString("keycloak.authRealm")
33 |
34 | private val TOKEN_CACHE_KEY = "keycloak.token"
35 |
36 | def cachedToken(): Future[Token] = {
37 | val optionalToken = syncCacheApi.get[Token](TOKEN_CACHE_KEY)
38 | if (optionalToken.isEmpty) {
39 | getToken()
40 | } else {
41 | Future(optionalToken.get)
42 | }
43 |
44 | }
45 |
46 | def getToken(): Future[Token] =
47 | wsClient
48 | .url(s"$URL$BASE_PATH/realms/$AUTH_REALM/protocol/openid-connect/token")
49 | .addHttpHeaders("Content-Type" -> "application/x-www-form-urlencoded")
50 | .post(
51 | Map(
52 | "client_id" -> CLIENT_ID,
53 | "client_secret" -> CLIENT_SECRET,
54 | "grant_type" -> "client_credentials"
55 | )
56 | )
57 | .map { case AhcWSResponse(underlying) =>
58 | if (underlying.status == 200) {
59 | val token = Json.parse(underlying.body).as[Token]
60 | syncCacheApi.set(TOKEN_CACHE_KEY, token, token.expires_in.seconds)
61 | token
62 | } else {
63 | val errorMessage = s"Cannot resolve client token ${underlying.status} | ${underlying.body}"
64 | println(errorMessage)
65 | throw new InvalidKeyCloakAuth(errorMessage)
66 | }
67 | }
68 |
69 | }
70 |
--------------------------------------------------------------------------------
/.github/workflows/test.yml:
--------------------------------------------------------------------------------
1 | name: Run Tests
2 | on:
3 | push:
4 | branches: [ master ]
5 | pull_request:
6 | branches: [ master ]
7 | jobs:
8 | test:
9 | runs-on: ubuntu-latest
10 | services:
11 | postgres:
12 | image: mdillon/postgis:latest
13 | env:
14 | POSTGRES_USER: test
15 | POSTGRES_DB: test
16 | POSTGRES_PASSWORD: test
17 | options: >-
18 | --health-cmd pg_isready
19 | --health-interval 10s
20 | --health-timeout 5s
21 | --health-retries 5
22 | ports:
23 | - 5432:5432
24 | steps:
25 | - uses: actions/checkout@v2
26 | - name: Cache SBT ivy cache
27 | uses: actions/cache@v1
28 | with:
29 | path: ~/.ivy2/cache
30 | key: ${{ runner.os }}-sbt-ivy-cache-${{ hashFiles('**/build.sbt') }}
31 | - name: Cache SBT
32 | uses: actions/cache@v1
33 | with:
34 | path: ~/.sbt
35 | key: ${{ runner.os }}-sbt-${{ hashFiles('**/build.sbt') }}
36 | - name: Set up JDK 11.0.9
37 | uses: actions/setup-java@v1
38 | with:
39 | java-version: 11.0.9
40 | - name: Set up Firebase
41 | run: npm install -g firebase-tools
42 | - name: Set up Environment
43 | shell: bash
44 | run: |
45 | cat <<< $FIREBASE_JSON > ./conf/firebase.json
46 | env:
47 | FIREBASE_JSON: ${{ secrets.FIREBASE_JSON_PRODUCTION }}
48 | - name: Run tests
49 | run: |
50 | cd .bin
51 | firebase emulators:exec "cd .. && export FIREBASE_AUTH_EMULATOR_HOST='localhost:9099' && sbt ciTests" --only auth --project demo-bootstrap-play2 --import=./firebase-data
52 | env:
53 | CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
54 | GITHUB_TOKEN: ${{ secrets.PUBLIC_GITHUB_TOKEN }}
55 | - name: Visualize DB Schema
56 | run: |
57 | pushd $PWD/.bin/schemaspy/
58 | curl -JLO https://search.maven.org/remotecontent?filepath=org/postgresql/postgresql/$POSTGRESQL_VERSION/postgresql-$POSTGRESQL_VERSION.jar
59 | curl -JL https://github.com/schemaspy/schemaspy/releases/download/v${SCHEMASPY_VERSION}/schemaspy-${SCHEMASPY_VERSION}.jar -o schemaspy.jar
60 | java -jar schemaspy.jar -vizjs
61 | zip -r schemaspy.zip output/
62 | popd
63 | env:
64 | POSTGRESQL_VERSION: "42.5.0"
65 | SCHEMASPY_VERSION: "6.1.0"
66 | - name: Archive schemaspy db documentation
67 | uses: actions/upload-artifact@v3
68 | with:
69 | name: database-documentation
70 | path: .bin/schemaspy/schemaspy.zip
71 |
--------------------------------------------------------------------------------
/app/de/innfactory/bootstrapplay2/commons/jwt/ConfigurableJWTValidator.scala:
--------------------------------------------------------------------------------
1 | package de.innfactory.bootstrapplay2.commons.jwt
2 |
3 | import java.text.ParseException
4 | import com.nimbusds.jose.jwk.source.JWKSource
5 | import com.nimbusds.jose.proc.{JWSVerificationKeySelector, SecurityContext}
6 | import com.nimbusds.jwt.JWTClaimsSet
7 | import com.nimbusds.jwt.proc.{BadJWTException, DefaultJWTClaimsVerifier, DefaultJWTProcessor}
8 | import de.innfactory.bootstrapplay2.commons.jwt.algorithm.JWTAlgorithm
9 | import de.innfactory.bootstrapplay2.commons.jwt.algorithm.JWTAlgorithm.RS256
10 | import de.innfactory.play.smithy4play.JWTToken
11 |
12 | object ConfigurableJWTValidator {
13 | def apply(
14 | keySource: JWKSource[SecurityContext],
15 | algorithm: JWTAlgorithm = RS256,
16 | maybeCtx: Option[SecurityContext] = None,
17 | additionalValidations: List[(JWTClaimsSet, SecurityContext) => Option[BadJWTException]] = List.empty
18 | ): ConfigurableJWTValidator = new ConfigurableJWTValidator(keySource, algorithm, maybeCtx, additionalValidations)
19 | }
20 |
21 | final class ConfigurableJWTValidator(
22 | keySource: JWKSource[SecurityContext],
23 | algorithm: JWTAlgorithm = RS256,
24 | maybeCtx: Option[SecurityContext] = None,
25 | additionalValidations: List[(JWTClaimsSet, SecurityContext) => Option[BadJWTException]] = List.empty
26 | ) extends JWTValidator {
27 |
28 | private val jwtProcessor = new DefaultJWTProcessor[SecurityContext]
29 | private val keySelector = new JWSVerificationKeySelector[SecurityContext](algorithm.nimbusRepresentation, keySource)
30 | jwtProcessor.setJWSKeySelector(keySelector)
31 |
32 | jwtProcessor.setJWTClaimsSetVerifier(new DefaultJWTClaimsVerifier[SecurityContext] {
33 | override def verify(claimsSet: JWTClaimsSet, context: SecurityContext): Unit = {
34 | super.verify(claimsSet, context)
35 |
36 | additionalValidations
37 | .to(LazyList)
38 | .map(f => f(claimsSet, context))
39 | .collect { case Some(e) => e }
40 | .foreach(e => throw e)
41 | }
42 | })
43 |
44 | private val ctx: SecurityContext = maybeCtx.orNull
45 |
46 | override def validate(jwtToken: JWTToken): Either[BadJWTException, (JWTToken, JWTClaimsSet)] = {
47 | val content: String = jwtToken.content
48 | if (content.isEmpty) Left(EmptyJwtTokenContent)
49 | else
50 | try {
51 | val claimsSet = jwtProcessor.process(content, ctx)
52 | Right(jwtToken -> claimsSet)
53 | } catch {
54 | case e: BadJWTException => Left(e)
55 | case _: ParseException => Left(InvalidJwtToken)
56 | case e: Exception => Left(UnknownException(e))
57 | }
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/doc/innFactoryIcon.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/.bin/setup/src/main/scala/packagedomain/scalafiles/Service.scala:
--------------------------------------------------------------------------------
1 | package packagedomain.scalafiles
2 |
3 | import packagedomain.common.CrudHelper
4 | import config.SetupConfig
5 |
6 | case class Service(packageDomain: String, packageName: String) extends ScalaDomainFile with CrudHelper {
7 | override def subPath =
8 | s"/$packageName/domain/interfaces/"
9 | val name = s"${packageDomain.capitalize}Service"
10 | override def getContent(withCrud: Boolean)(implicit config: SetupConfig): String = {
11 | val domainModel = DomainModel(packageDomain, packageName)
12 | val domainModelId = DomainModelId(packageDomain, packageName)
13 | val domainService = DomainService(packageDomain, packageName)
14 | val content = s"""
15 | |package ${config.project.getNamespace()}.$packageName.domain.interfaces
16 | |
17 | |${CrudImportsKey}
18 | |import com.google.inject.ImplementedBy
19 | |import ${config.project.getNamespace()}.$packageName.domain.services.${domainService.name}
20 | |
21 | |import scala.concurrent.Future
22 | |
23 | |@ImplementedBy(classOf[${domainService.name}])
24 | |trait $name {
25 | | ${CrudLogicKey}
26 | |}
27 | |""".stripMargin
28 |
29 | replaceForCrud(
30 | content,
31 | withCrud,
32 | createCrudLogic(domainModel, domainModelId),
33 | createCrudImports(domainModel, domainModelId)
34 | )
35 | }
36 |
37 | private def createCrudImports(domainModel: DomainModel, domainModelId: DomainModelId)(implicit
38 | config: SetupConfig
39 | ): String =
40 | s"""
41 | |import ${config.project.getNamespace()}.$packageName.domain.models.{${domainModelId.name}, ${domainModel.name}}
42 | |import de.innfactory.play.controller.ResultStatus
43 | |import cats.data.EitherT
44 | |import ${config.project.getNamespace()}.commons.RequestContextWithUser
45 | |""".stripMargin
46 |
47 | private def createCrudLogic(domainModel: DomainModel, domainModelId: DomainModelId): String =
48 | s"""
49 | | def getAll()(implicit rc: RequestContextWithUser): EitherT[Future, ResultStatus, Seq[${domainModel.name}]]
50 | | def getById(id: ${domainModelId.name})(implicit rc: RequestContextWithUser): EitherT[Future, ResultStatus, ${domainModel.name}]
51 | | def update(${domainModel
52 | .nameLowerCased()}: ${domainModel.name})(implicit rc: RequestContextWithUser): EitherT[Future, ResultStatus, ${domainModel.name}]
53 | | def create(${domainModel
54 | .nameLowerCased()}: ${domainModel.name})(implicit rc: RequestContextWithUser): EitherT[Future, ResultStatus, ${domainModel.name}]
55 | | def delete(id: ${domainModelId.name})(implicit rc: RequestContextWithUser): EitherT[Future, ResultStatus, Boolean]
56 | |""".stripMargin
57 | }
58 |
--------------------------------------------------------------------------------
/.bin/setup/src/main/scala/packagedomain/scalafiles/Repository.scala:
--------------------------------------------------------------------------------
1 | package packagedomain.scalafiles
2 |
3 | import packagedomain.common.CrudHelper
4 | import packagedomain.smithyfiles.ApiDefinition
5 | import config.SetupConfig
6 |
7 | case class Repository(packageDomain: String, packageName: String) extends ScalaDomainFile with CrudHelper {
8 | override def subPath =
9 | s"/$packageName/domain/interfaces/"
10 | val name = s"${packageDomain.capitalize}Repository"
11 | override def getContent(withCrud: Boolean)(implicit config: SetupConfig): String = {
12 | val domainModel = DomainModel(packageDomain, packageName)
13 | val domainModelId = DomainModelId(packageDomain, packageName)
14 | val domainRepository = SlickRepository(packageDomain, packageName)
15 | val content = s"""
16 | |package ${config.project.getNamespace()}.$packageName.domain.interfaces
17 | |
18 | |import com.google.inject.ImplementedBy
19 | |import ${config.project.getNamespace()}.$packageName.infrastructure.${domainRepository.name}
20 | |${CrudImportsKey}
21 | |
22 | |@ImplementedBy(classOf[${domainRepository.name}])
23 | |private[$packageName] trait $name {
24 | | ${CrudLogicKey}
25 | |}
26 | |""".stripMargin
27 |
28 | replaceForCrud(
29 | content,
30 | withCrud,
31 | createCrudLogic(domainModel, domainModelId),
32 | createCrudImports(domainModel, domainModelId)
33 | )
34 | }
35 |
36 | private def createCrudImports(domainModel: DomainModel, domainModelId: DomainModelId)(implicit
37 | config: SetupConfig
38 | ): String =
39 | s"""
40 | |import ${config.project.getNamespace()}.$packageName.domain.models.{${domainModel.name}, ${domainModelId.name}}
41 | |import de.innfactory.play.controller.ResultStatus
42 | |import cats.data.EitherT
43 | |import de.innfactory.play.tracing.TraceContext
44 | |import scala.concurrent.Future
45 | |""".stripMargin
46 |
47 | private def createCrudLogic(domainModel: DomainModel, domainModelId: DomainModelId): String =
48 | s"""
49 | | def getAll()(implicit rc: TraceContext): EitherT[Future, ResultStatus, Seq[${domainModel.name}]]
50 | | def getById(id: ${domainModelId.name})(implicit rc: TraceContext): EitherT[Future, ResultStatus, ${domainModel.name}]
51 | | def create(${domainModel
52 | .nameLowerCased()}: ${domainModel.name})(implicit rc: TraceContext): EitherT[Future, ResultStatus, ${domainModel.name}]
53 | | def update(${domainModel
54 | .nameLowerCased()}: ${domainModel.name})(implicit rc: TraceContext): EitherT[Future, ResultStatus, ${domainModel.name}]
55 | | def delete(id: ${domainModelId.name})(implicit rc: TraceContext): EitherT[Future, ResultStatus, Boolean]
56 | |""".stripMargin
57 |
58 | }
59 |
--------------------------------------------------------------------------------
/conf/application.conf:
--------------------------------------------------------------------------------
1 | include "akka"
2 |
3 | bootstrap-play2 {
4 | profile = "slick.jdbc.PostgresProfile$"
5 | database = {
6 |
7 | urlPrefix = "jdbc:postgresql://"
8 | urlPrefix = ${?URL_PREFIX}
9 |
10 | # Database Host
11 | host = "localhost"
12 | host = ${?DATABASE_HOST}
13 |
14 | # Database DB Name
15 | db = "test"
16 | db = ${?DATABASE_DB}
17 |
18 | # Database Port
19 | port = "5432"
20 | port = ${?DATABASE_PORT}
21 |
22 | url = ${?bootstrap-play2.database.urlPrefix}""${?bootstrap-play2.database.host}":"${?bootstrap-play2.database.port}"/"${?bootstrap-play2.database.db}
23 | url = ${?DATABASE_URL}
24 |
25 | # Database User and Password
26 | user = "test"
27 | user = ${?DATABASE_USER}
28 | password = "test"
29 | password = ${?DATABASE_PASSWORD}
30 |
31 | // -- SETTINGS --
32 |
33 | driver = org.postgresql.Driver
34 |
35 | queueSize = 100
36 |
37 | numThreads = 4
38 | maxThreads = 4
39 | maxConnections = 8
40 |
41 | connectionTimeout = 7000
42 | validationTimeout = 7000
43 | }
44 | }
45 |
46 | smithy4play.autoRoutePackage = "de.innfactory.bootstrapplay2"
47 |
48 | // ERROR HANDLER
49 |
50 | # Override default error handler
51 | play.http.errorHandler = "de.innfactory.bootstrapplay2.commons.errors.ErrorHandler"
52 |
53 | // FIREBASE
54 |
55 | firebase.file = "firebase.json"
56 | firebase.file = ${?FIREBASE_FILEPATH}
57 |
58 | // GCP
59 |
60 | gcp.serviceAccount = "gcp-backend-sa.json"
61 | gcp.serviceAccount = ${?GCP_CONFIG}
62 |
63 | // PLAY SECRET
64 |
65 | play.http.secret.key = "KE;PMNWm/SGwA?IU=OqznzyyR7hFFpET0:z=rjBl:aI4UY@@Ji_mia/>Ai9@9rRR"
66 | play.http.secret.key = ${?PLAY_HTTP_SECRET_KEY}
67 |
68 | // FILTERS
69 |
70 | play.filters.enabled = [ "play.filters.cors.CORSFilter" ]
71 |
72 | play.filters.cors {
73 | pathPrefixes = ["/v1/"]
74 | allowedOrigins = null
75 | allowedHttpMethods = ["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD"]
76 | preflightMaxAge = 3 days
77 | supportCredentials = true
78 | exposedHeaders = ["Access-Control-Allow-Origin"]
79 | }
80 |
81 | // Access Logging (Which http status codes should be logged)
82 |
83 | logging.access.statusList = [404,403,401]
84 | logging.access.statusList = ${?LOGGING_STATUSLIST}
85 |
86 | http.port = 8080
87 |
88 | project.id = "bootstrap-play2"
89 | project.id = ${?PROJECT_ID}
90 |
91 | keycloak {
92 | clientSecret = ""
93 | clientSecret = ${?KEYCLOAK_CLIENT_SECRET}
94 | clientId = ""
95 | clientId = ${?KEYCLOAK_CLIENT_ID}
96 | url = ""
97 | url = ${?KEYCLOAK_URL}
98 | basePath = ""
99 | basePath = ${?KEYCLOAK_BASE_PATH}
100 | authRealm = "dev"
101 | authRealm = ${?KEYCLOAK_REALM}
102 | realm = "dev"
103 | realm = ${?KEYCLOAK_REALM}
104 | }
--------------------------------------------------------------------------------
/test/controllers/LocationsControllerTest.scala:
--------------------------------------------------------------------------------
1 | package controllers
2 |
3 | import de.innfactory.bootstrapplay2.api.{LocationAPIControllerGen, LocationRequestBody}
4 | import de.innfactory.smithy4play.client.GenericAPIClient.EnhancedGenericAPIClient
5 | import org.scalatestplus.play.{BaseOneAppPerSuite, PlaySpec}
6 | import de.innfactory.smithy4play.client.SmithyPlayTestUtils._
7 | import testutils.FakeRequestClient
8 |
9 | class LocationsControllerTest extends PlaySpec with BaseOneAppPerSuite with TestApplicationFactory {
10 | private val companyAdminLocationClient = LocationAPIControllerGen.withClientAndHeaders(
11 | new FakeRequestClient(),
12 | Some(Map("Authorization" -> Seq(authUtils.CompanyAdminEmailToken)))
13 | )
14 |
15 | /** ———————————————— */
16 | /** LOCATIONS */
17 | /** ———————————————— */
18 | "LocationsController" must {
19 | "get by id" in {
20 | val result =
21 | companyAdminLocationClient.getLocationById("592c5187-cb85-4b66-b0fc-293989923e1e").awaitRight
22 | result.statusCode mustBe result.expectedStatusCode
23 | }
24 |
25 | "get by company" in {
26 | val result =
27 | companyAdminLocationClient.getAllLocationsByCompany("0ce84627-9a66-46bf-9a1d-4f38b82a38e3").awaitRight
28 | result.statusCode mustBe result.expectedStatusCode
29 | }
30 |
31 | "get all" in {
32 | val result = companyAdminLocationClient.getAllLocations().awaitRight
33 | result.statusCode mustBe result.expectedStatusCode
34 | }
35 |
36 | "post" in {
37 | val result = companyAdminLocationClient
38 | .createLocation(
39 | LocationRequestBody(
40 | company = "0ce84627-9a66-46bf-9a1d-4f38b82a38e3",
41 | name = Some("test")
42 | )
43 | )
44 | .awaitRight
45 | result.statusCode mustBe result.expectedStatusCode
46 | }
47 |
48 | "patch" in {
49 | val result = companyAdminLocationClient
50 | .updateLocation(
51 | LocationRequestBody(
52 | id = Some(de.innfactory.bootstrapplay2.api.LocationId("592c5187-cb85-4b66-b0fc-293989923e1e")),
53 | company = "0ce84627-9a66-46bf-9a1d-4f38b82a38e3",
54 | name = Some("test2")
55 | )
56 | )
57 | .awaitRight
58 | result.statusCode mustBe result.expectedStatusCode
59 | }
60 |
61 | "delete" in {
62 | val successfulDelete = companyAdminLocationClient
63 | .deleteLocation(
64 | de.innfactory.bootstrapplay2.api.LocationId("592c5187-cb85-4b66-b0fc-293989923e1e")
65 | )
66 | .awaitRight
67 | successfulDelete.statusCode mustBe successfulDelete.expectedStatusCode
68 | val deletedAfterDelete = companyAdminLocationClient
69 | .deleteLocation(
70 | de.innfactory.bootstrapplay2.api.LocationId("592c5187-cb85-4b66-b0fc-293989923e1e")
71 | )
72 | .awaitLeft
73 | deletedAfterDelete.statusCode mustBe 404
74 | }
75 | }
76 | }
77 |
--------------------------------------------------------------------------------