├── .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 | --------------------------------------------------------------------------------