├── web_client
├── public
│ └── .gitkeep
├── src
│ └── main
│ │ ├── resources
│ │ └── MainPage.css
│ │ ├── graphql
│ │ ├── allproduct.graphql
│ │ ├── example_mutation.graphql
│ │ └── example.graphql
│ │ └── scala
│ │ ├── com
│ │ └── mypackage
│ │ │ ├── util
│ │ │ └── UrlUtils.scala
│ │ │ ├── product
│ │ │ ├── Products.scala
│ │ │ ├── ProductDisplay.scala
│ │ │ └── ProductForm.scala
│ │ │ ├── App.scala
│ │ │ ├── MainPage.scala
│ │ │ ├── Bootstrap.scala
│ │ │ └── authentication
│ │ │ └── Login.scala
│ │ ├── reactrouter
│ │ ├── Redirect.scala
│ │ ├── BrowserRouter.scala
│ │ ├── NavLink.scala
│ │ └── Route.scala
│ │ └── antd
│ │ ├── Message.scala
│ │ ├── Button.scala
│ │ ├── Input.scala
│ │ ├── Grid.scala
│ │ ├── Card.scala
│ │ ├── Layout.scala
│ │ ├── Menu.scala
│ │ └── Form.scala
└── webpack
│ ├── scalajs-entry.js
│ ├── webpack-opt.config.js
│ ├── fastopt-loader.js
│ ├── webpack-fastopt.config.js
│ └── webpack-core.config.js
├── project
├── build.properties
├── ProjectVersionManager.scala
├── Schema.scala
├── Packaging.scala
├── PlayUtils.scala
├── plugins.sbt
├── WebpackUtils.scala
├── QuickCompiler.scala
├── Server.scala
├── WebClient.scala
├── Shared.scala
├── GraphqlUtils.scala
└── DatabaseUtils.scala
├── server
├── conf
│ ├── messages
│ ├── routes
│ ├── application.conf
│ ├── logback.xml
│ └── evolutions
│ │ └── default
│ │ └── 1.sql
├── app
│ ├── auth
│ │ ├── User.scala
│ │ ├── AuthEnvironment.scala
│ │ ├── UserIdentityService.scala
│ │ ├── ManualUserGenerator.scala
│ │ ├── UserSessionRepository.scala
│ │ └── PasswordInfoRepository.scala
│ ├── views
│ │ ├── index.scala.html
│ │ ├── main.scala.html
│ │ └── graphiql.scala.html
│ ├── repositories
│ │ ├── PictureRepo.scala
│ │ └── ProductRepo.scala
│ ├── infrastructure
│ │ ├── JodaAwareSourceCodeGenerator.scala
│ │ └── ApplyEvolutions.scala
│ ├── controllers
│ │ ├── HomeController.scala
│ │ ├── AuthenticationController.scala
│ │ └── GraphQLController.scala
│ ├── Bootstrap.scala
│ └── di
│ │ └── DiModule.scala
└── public
│ └── images
│ ├── nutella.jpeg
│ ├── cheesecake1.jpeg
│ ├── cheesecake2.jpeg
│ └── default_item.jpg
├── schema
└── src
│ └── main
│ └── scala
│ └── com
│ └── mypackage
│ ├── RequestContext.scala
│ ├── PictureRepoLike.scala
│ ├── ProductRepoLike.scala
│ └── GraphQLSchema.scala
├── shared
└── src
│ └── main
│ └── scala
│ └── com
│ └── mypackage
│ └── Domain.scala
├── .gitignore
├── docker-compose.yaml
├── LICENSE.md
└── README.md
/web_client/public/.gitkeep:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/project/build.properties:
--------------------------------------------------------------------------------
1 | sbt.version=1.2.8
2 |
--------------------------------------------------------------------------------
/server/conf/messages:
--------------------------------------------------------------------------------
1 | # https://www.playframework.com/documentation/latest/ScalaI18N
2 |
--------------------------------------------------------------------------------
/web_client/src/main/resources/MainPage.css:
--------------------------------------------------------------------------------
1 | .app-title {
2 | color: #0F0;
3 | }
4 |
--------------------------------------------------------------------------------
/server/app/auth/User.scala:
--------------------------------------------------------------------------------
1 | package auth
2 |
3 | import com.mohiva.play.silhouette.api.Identity
4 |
5 | case class User() extends Identity
6 |
--------------------------------------------------------------------------------
/server/public/images/nutella.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/espinhogr/scala-graphql-fullstack-seed/HEAD/server/public/images/nutella.jpeg
--------------------------------------------------------------------------------
/server/public/images/cheesecake1.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/espinhogr/scala-graphql-fullstack-seed/HEAD/server/public/images/cheesecake1.jpeg
--------------------------------------------------------------------------------
/server/public/images/cheesecake2.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/espinhogr/scala-graphql-fullstack-seed/HEAD/server/public/images/cheesecake2.jpeg
--------------------------------------------------------------------------------
/server/public/images/default_item.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/espinhogr/scala-graphql-fullstack-seed/HEAD/server/public/images/default_item.jpg
--------------------------------------------------------------------------------
/schema/src/main/scala/com/mypackage/RequestContext.scala:
--------------------------------------------------------------------------------
1 | package com.mypackage
2 |
3 | trait RequestContext {
4 |
5 | def productRepo: ProductRepoLike
6 |
7 | def pictureRepo: PictureRepoLike
8 | }
9 |
--------------------------------------------------------------------------------
/web_client/src/main/graphql/allproduct.graphql:
--------------------------------------------------------------------------------
1 | query AllProducts {
2 | products {
3 | id
4 | name
5 | description
6 | pictures(size: 500) {
7 | width
8 | height
9 | url
10 | }
11 | }
12 | }
--------------------------------------------------------------------------------
/web_client/src/main/graphql/example_mutation.graphql:
--------------------------------------------------------------------------------
1 | mutation AddProduct($name: String!, $description: String!) {
2 | addProduct(name: $name, description: $description) {
3 | id
4 | name
5 | description
6 | }
7 | }
--------------------------------------------------------------------------------
/web_client/src/main/graphql/example.graphql:
--------------------------------------------------------------------------------
1 | query MyProduct($id: String!, $size: Int!) {
2 | product(id: $id) {
3 | name
4 | pictures(size: $size) {
5 | width
6 | height
7 | url
8 | }
9 | }
10 | }
11 |
12 |
--------------------------------------------------------------------------------
/schema/src/main/scala/com/mypackage/PictureRepoLike.scala:
--------------------------------------------------------------------------------
1 | package com.mypackage
2 |
3 | import com.mypackage.Domain.Picture
4 |
5 | import scala.concurrent.Future
6 |
7 | trait PictureRepoLike {
8 |
9 | def picturesByProduct(id: String): Future[Seq[Picture]]
10 |
11 | }
12 |
--------------------------------------------------------------------------------
/server/app/auth/AuthEnvironment.scala:
--------------------------------------------------------------------------------
1 | package auth
2 |
3 | import com.mohiva.play.silhouette.api.Env
4 | import com.mohiva.play.silhouette.impl.authenticators.BearerTokenAuthenticator
5 |
6 | trait AuthEnvironment extends Env {
7 | type A = BearerTokenAuthenticator
8 | type I = User
9 | }
10 |
--------------------------------------------------------------------------------
/shared/src/main/scala/com/mypackage/Domain.scala:
--------------------------------------------------------------------------------
1 | package com.mypackage
2 |
3 | object Domain {
4 |
5 | trait Identifiable {
6 | def id: String
7 | }
8 |
9 | case class Picture(width: Int, height: Int, url: Option[String])
10 |
11 | case class Product(id: String, name: String, description: String) extends Identifiable
12 |
13 | }
14 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *~
2 | *\.ZILESAVE
3 | *\.tmp
4 | *\.log
5 | *\.out
6 | /\.classpath
7 | /\.project
8 | /\.settings
9 | /\.idea
10 | /\.idea_modules
11 | /\.lib
12 | *\.iml
13 | \.classpath_nb
14 | \.ensime
15 | \.ensime_lucene
16 | \.ensime_cache/
17 | target/
18 | dist/
19 | lib_managed/
20 | src_managed/
21 | project/boot/
22 | project/plugins/project/
23 | .history
24 | .cache
25 | .lib/
26 | .idea/
27 | logs/
28 | lib/
29 | tmp/
30 |
--------------------------------------------------------------------------------
/schema/src/main/scala/com/mypackage/ProductRepoLike.scala:
--------------------------------------------------------------------------------
1 | package com.mypackage
2 |
3 | import com.mypackage.Domain.{Product => RetailProduct}
4 |
5 | import scala.concurrent.Future
6 |
7 | trait ProductRepoLike {
8 |
9 | def product(id: String): Future[Option[RetailProduct]]
10 |
11 | def products: Future[Seq[RetailProduct]]
12 |
13 | def addProduct(name: String, description: String): Future[RetailProduct]
14 |
15 | }
16 |
--------------------------------------------------------------------------------
/server/app/views/index.scala.html:
--------------------------------------------------------------------------------
1 | @(devMode: Boolean)
2 |
3 | @main("Welcome to Play with Scala.js and React")(Html("")) {
4 |
7 |
8 |
9 | }
10 |
--------------------------------------------------------------------------------
/web_client/webpack/scalajs-entry.js:
--------------------------------------------------------------------------------
1 | if (process.env.NODE_ENV === "production") {
2 | const opt = require("./web_client-opt.js");
3 | opt.entrypoint.main();
4 | module.exports = opt;
5 | } else {
6 | var exports = window;
7 | exports.require = require("./web_client-fastopt-entrypoint.js").require;
8 | window.global = window;
9 |
10 | const fastOpt = require("./web_client-fastopt.js");
11 | fastOpt.entrypoint.main()
12 | module.exports = fastOpt;
13 |
14 | if (module.hot) {
15 | module.hot.accept();
16 | }
17 | }
--------------------------------------------------------------------------------
/server/app/auth/UserIdentityService.scala:
--------------------------------------------------------------------------------
1 | package auth
2 |
3 | import com.mohiva.play.silhouette.api.LoginInfo
4 | import com.mohiva.play.silhouette.api.services.IdentityService
5 |
6 | import scala.concurrent.Future
7 |
8 | class UserIdentityService extends IdentityService[User] {
9 |
10 | /**
11 | * This is not implemented as I'm not saving any data related to the user,
12 | * this is useful to save things like email, username, etc etc.
13 | */
14 | override def retrieve(loginInfo: LoginInfo): Future[Option[User]] = {
15 | Future.successful(Some(User()))
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/web_client/webpack/webpack-opt.config.js:
--------------------------------------------------------------------------------
1 | var merge = require('webpack-merge');
2 | var core = require('./webpack-core.config.js')
3 | var webpack = require("webpack");
4 |
5 | var generatedConfig = require("./scalajs.webpack.config.js");
6 | const entries = {};
7 | entries[Object.keys(generatedConfig.entry)[0]] = "scalajs";
8 |
9 | module.exports = merge(core, {
10 | mode: "production",
11 | devtool: "source-map",
12 | entry: entries,
13 | plugins: [
14 | new webpack.DefinePlugin({
15 | 'process.env': {
16 | NODE_ENV: JSON.stringify('production')
17 | }
18 | })
19 | ]
20 | })
--------------------------------------------------------------------------------
/server/app/views/main.scala.html:
--------------------------------------------------------------------------------
1 | @*
2 | * This template is called from the `index` template. This template
3 | * handles the rendering of the page header and body tags. It takes
4 | * two arguments, a `String` for the title of the page and an `Html`
5 | * object to insert into the body of the page.
6 | *@
7 | @(title: String)(headers: Html)(content: Html)
8 |
9 |
10 |
11 |
12 |
13 |
14 | @headers
15 | @title
16 |
17 |
18 | @content
19 |
20 |
21 |
22 |
--------------------------------------------------------------------------------
/server/app/repositories/PictureRepo.scala:
--------------------------------------------------------------------------------
1 | package repositories
2 |
3 | import com.mypackage.Domain.Picture
4 | import com.mypackage.PictureRepoLike
5 | import database.Tables
6 | import javax.inject.Inject
7 | import slick.jdbc.MySQLProfile.api._
8 |
9 | import scala.concurrent.ExecutionContext.Implicits.global
10 | import scala.concurrent.Future
11 |
12 | class PictureRepo @Inject()(db: Database) extends PictureRepoLike {
13 |
14 | override def picturesByProduct(id: String): Future[Seq[Picture]] = {
15 | val action = Tables.Picture.filter(_.productid === id.toInt).result
16 | db.run(action).map(_.map(p => Picture(p.width, p.height, p.url)))
17 | }
18 |
19 | }
20 |
--------------------------------------------------------------------------------
/docker-compose.yaml:
--------------------------------------------------------------------------------
1 | version: '3'
2 | services:
3 |
4 | # postgres:
5 | # image: postgres:11.3
6 | # restart: always
7 | # environment:
8 | # POSTGRES_USER: admin
9 | # POSTGRES_PASSWORD: admin
10 | # ports:
11 | # - 5432:5432
12 | # volumes:
13 | # - postgres:/var/lib/postgresql/data
14 |
15 | mysql:
16 | image: mysql:5.7.26
17 | environment:
18 | MYSQL_DATABASE: test
19 | MYSQL_ROOT_PASSWORD: root
20 | MYSQL_USER: admin
21 | MYSQL_PASSWORD: admin
22 | ports:
23 | - 3306:3306
24 | volumes:
25 | - mysql:/var/lib/mysql
26 | restart: always
27 |
28 | volumes:
29 | postgres: ~
30 | mysql: ~
31 |
--------------------------------------------------------------------------------
/web_client/webpack/fastopt-loader.js:
--------------------------------------------------------------------------------
1 | /*
2 | * Processes the source file before it's bundled and it resolves the path of the
3 | * map file.
4 | */
5 | const fs = require("fs");
6 | const path = require("path");
7 | module.exports = function(source) {
8 | const sourceMapPath = source.substring(
9 | source.lastIndexOf("//# sourceMappingURL=") + "//# sourceMappingURL=".length
10 | ).trim();
11 |
12 | const map = JSON.parse(fs.readFileSync(sourceMapPath, { encoding: "utf-8" }));
13 | map.sources = map.sources.map(s => {
14 | if (s.startsWith("http://") || s.startsWith("https://")) {
15 | return s;
16 | } else {
17 | return "file://" + path.resolve(s);
18 | }
19 | })
20 |
21 | this.callback(null,
22 | source,
23 | JSON.stringify(map)
24 | );
25 | };
--------------------------------------------------------------------------------
/server/conf/routes:
--------------------------------------------------------------------------------
1 | # Routes
2 | # This file defines all application routes (Higher priority routes first)
3 | # https://www.playframework.com/documentation/latest/ScalaRouting
4 | # ~~~~
5 |
6 | POST /authenticate controllers.AuthenticationController.authenticate
7 |
8 | POST /graphql controllers.GraphQLController.graphql
9 |
10 | GET /graphiql controllers.GraphQLController.graphiQL
11 |
12 | GET /assets/*file controllers.Assets.versioned(path="/public", file: Asset)
13 |
14 | GET / controllers.HomeController.index()
15 |
16 | # This route has to be the last one because it catches all the paths
17 | GET /*dynamic controllers.HomeController.dynamicPath(dynamic)
18 |
--------------------------------------------------------------------------------
/web_client/src/main/scala/com/mypackage/util/UrlUtils.scala:
--------------------------------------------------------------------------------
1 | package com.mypackage.util
2 |
3 | import scala.scalajs.js
4 |
5 | object UrlUtils {
6 |
7 | /**
8 | * Extracts the base url form the address bar.
9 | * Returns only the part with the protocol, the domain and the port
10 | *
11 | */
12 | def getBaseUrl: String = {
13 | val currentPageUrl: String = js.Dynamic.global.window.location.href.asInstanceOf[String]
14 | val protocolSlashIndex = currentPageUrl.indexOf('/')
15 | val baseUrlSlashIndex = currentPageUrl.indexOf('/', protocolSlashIndex + 2)
16 |
17 |
18 | currentPageUrl.substring(0, baseUrlSlashIndex)
19 | }
20 |
21 | def navigate(queryString: String) = {
22 | js.Dynamic.global.window.location.href = getBaseUrl + queryString
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/web_client/src/main/scala/reactrouter/Redirect.scala:
--------------------------------------------------------------------------------
1 | package reactrouter
2 |
3 | import slinky.core.ExternalComponent
4 | import slinky.core.ExternalComponentWithAttributes
5 | import slinky.web.html.a
6 |
7 | import scala.scalajs.js
8 | import scala.scalajs.js.UndefOr
9 | import scala.scalajs.js.annotation.JSImport
10 | import scala.scalajs.js.|
11 |
12 | @JSImport("react-router-dom", JSImport.Default)
13 | @js.native
14 | object ReactRedirect extends js.Object {
15 | val Redirect: js.Object = js.native
16 | }
17 |
18 | object Redirect extends ExternalComponent {
19 | case class Props(push: Boolean = false,
20 | exact: Boolean = false,
21 | strict: Boolean = false,
22 | to: String | js.Object)
23 |
24 | override val component = ReactRedirect.Redirect
25 | }
26 |
--------------------------------------------------------------------------------
/web_client/src/main/scala/reactrouter/BrowserRouter.scala:
--------------------------------------------------------------------------------
1 | package reactrouter
2 |
3 | import slinky.core.ExternalComponent
4 | import slinky.core.annotations.react
5 |
6 | import scala.scalajs.js
7 | import scala.scalajs.js.UndefOr
8 | import scala.scalajs.js.annotation.JSImport
9 |
10 | @JSImport("react-router-dom", JSImport.Default)
11 | @js.native
12 | object ReactBrowserRouter extends js.Object {
13 | val BrowserRouter: js.Object = js.native
14 | }
15 |
16 | @react object BrowserRouter extends ExternalComponent {
17 | case class Props(basename: String = "/",
18 | getUserConfirmation: UndefOr[js.Function0[Boolean]] = js.undefined,
19 | forceRefresh: Boolean = false,
20 | keyLength: Int = 6)
21 |
22 | override val component = ReactBrowserRouter.BrowserRouter
23 | }
24 |
--------------------------------------------------------------------------------
/web_client/webpack/webpack-fastopt.config.js:
--------------------------------------------------------------------------------
1 | // Webpack-merge is used to merge this configuration with the core one
2 | var merge = require('webpack-merge');
3 | var core = require('./webpack-core.config.js')
4 |
5 | var generatedConfig = require("./scalajs.webpack.config.js");
6 | const entries = {};
7 | entries[Object.keys(generatedConfig.entry)[0]] = "scalajs";
8 |
9 | module.exports = merge(core, {
10 | devtool: "cheap-module-eval-source-map",
11 | entry: entries,
12 | devServer: {
13 | hot: true
14 | },
15 | module: {
16 | noParse: (content) => {
17 | return content.endsWith("-fastopt.js");
18 | },
19 | rules: [
20 | // Loader rule
21 | {
22 | test: /\-fastopt.js\$/,
23 | use: [ require.resolve('./fastopt-loader.js') ]
24 | }
25 | ]
26 | },
27 | output: {
28 | filename: '[name]-bundle.js'
29 | }
30 | })
--------------------------------------------------------------------------------
/project/ProjectVersionManager.scala:
--------------------------------------------------------------------------------
1 | import sbt._
2 | import sbt.Keys._
3 |
4 | object ProjectVersionManager {
5 | def writeConfig(projectId: String, projectName: String, projectPort: Int) = Def.task {
6 | val content =s"""
7 | |package version.util
8 | |
9 | |object Version {
10 | | val projectId = "$projectId"
11 | | val projectName = "$projectName"
12 | | val projectPort = $projectPort
13 | |
14 | | val version = "${version.value}"
15 | |}
16 | |""".stripMargin.trim
17 |
18 | val file = (sourceManaged in Compile).value / "version" / "util" / "Version.scala"
19 | val current = if(file.exists) { IO.read(file) } else { "" }
20 | if(current != content) { IO.write(file, content) }
21 | Seq(file)
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/project/Schema.scala:
--------------------------------------------------------------------------------
1 | import sbt.Keys._
2 | import sbt._
3 |
4 | /**
5 | * Project to hold the GraphQL schema.
6 | * This is not in shared because there are some parts used only by the server
7 | * and some used only by the client. This is mainly to the fact that the schema
8 | * is defined using Sangria DSL, then a schema is produced programmatically and
9 | * it's used in the client to generate the types.
10 | */
11 | object Schema {
12 |
13 | val schemaObject = settingKey[String]("Fully qualified name of the object containing the schema.")
14 |
15 | private[this] val schemaSettings = Shared.commonSettings ++ Seq(
16 | libraryDependencies ++= Seq(
17 | "org.sangria-graphql" %% "sangria" % "1.4.2",
18 | ),
19 |
20 | schemaObject := "com.mypackage.GraphQLSchema",
21 |
22 | )
23 |
24 | lazy val schema = (project in file("schema"))
25 | .settings(schemaSettings: _*)
26 | .dependsOn(Shared.sharedJvm)
27 | }
28 |
29 |
30 |
--------------------------------------------------------------------------------
/server/app/auth/ManualUserGenerator.scala:
--------------------------------------------------------------------------------
1 | package auth
2 |
3 | import com.mohiva.play.silhouette.api.LoginInfo
4 | import com.mohiva.play.silhouette.impl.providers.CredentialsProvider
5 | import di.DiModule._
6 | import slick.jdbc.JdbcBackend.Database
7 |
8 | import scala.concurrent.Await
9 | import scala.concurrent.duration._
10 |
11 | /**
12 | * This class is used in case you want to register a user manually.
13 | * It inserts a record in the database with the user credentials.
14 | */
15 | object ManualUserGenerator extends App {
16 |
17 | val username = "admin"
18 | val password = "admin"
19 |
20 | val database = Database.forConfig(databaseConfigPath)
21 | val passwordInfoRepository = new PasswordInfoRepository(database)
22 |
23 | val passwordInfo = hasher.hash(password)
24 | val loginInfo = LoginInfo(CredentialsProvider.ID, username)
25 | println(
26 | Await.result(passwordInfoRepository.add(loginInfo, passwordInfo), 5 seconds)
27 | )
28 | }
29 |
--------------------------------------------------------------------------------
/server/app/infrastructure/JodaAwareSourceCodeGenerator.scala:
--------------------------------------------------------------------------------
1 | package infrastructure
2 |
3 | import slick.codegen.SourceCodeGenerator
4 | import slick.model.Model
5 |
6 | /**
7 | * Slick source code generator which uses joda types instead of sql.Timestamp
8 | */
9 | class JodaAwareSourceCodeGenerator(model: Model) extends SourceCodeGenerator(model) {
10 |
11 | // This inserts the imports in any table, even if the table doesn't use DateTime, can be optimised.
12 | override def codePerTable: Map[String, String] = super.codePerTable.mapValues { v =>
13 | """import com.github.tototoshi.slick.MySQLJodaSupport._
14 | |import org.joda.time.DateTime
15 | |""".stripMargin + v
16 | }
17 |
18 | override def Table = new Table(_) {
19 | override def Column = new Column(_) {
20 |
21 | override def rawType = model.tpe match {
22 | case "java.sql.Timestamp" => "DateTime"
23 | case _ => super.rawType
24 | }
25 | }
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/server/app/controllers/HomeController.scala:
--------------------------------------------------------------------------------
1 | package controllers
2 |
3 | import javax.inject._
4 | import play.Environment
5 | import play.api.mvc._
6 |
7 | /**
8 | * This controller creates an `Action` to handle HTTP requests to the
9 | * application's home page.
10 | */
11 | @Singleton
12 | class HomeController @Inject()(cc: ControllerComponents, env: Environment) extends AbstractController(cc) {
13 |
14 | /**
15 | * Create an Action to render an HTML page.
16 | *
17 | * The configuration in the `routes` file means that this method
18 | * will be called when the application receives a `GET` request with
19 | * a path of `/`.
20 | */
21 | def index() = dynamicPath("")
22 |
23 | /**
24 | * This is used to be able to have single page applications on the javascript side
25 | * so that if they dynamically change the route, it's always the index being served.
26 | */
27 | def dynamicPath(dynamic: String) = Action { implicit request: Request[AnyContent] =>
28 | Ok(views.html.index(env.isDev))
29 | }
30 |
31 | }
32 |
--------------------------------------------------------------------------------
/web_client/src/main/scala/com/mypackage/product/Products.scala:
--------------------------------------------------------------------------------
1 | package com.mypackage.product
2 |
3 | import antd.AntForm
4 | import antd.Col
5 | import antd.LayoutContent
6 | import antd.Row
7 | import slinky.core.ReactComponentClass._
8 | import slinky.core.StatelessComponent
9 | import slinky.core.annotations.react
10 | import slinky.core.facade.React
11 | import slinky.web.html.h1
12 | import slinky.web.html.hr
13 | import slinky.web.html.style
14 |
15 | import scala.scalajs.js
16 |
17 | @react class Products extends StatelessComponent {
18 | type Props = Unit
19 |
20 | def render() = {
21 | val wrappedProductForm = AntForm.Form.create()(wrapperToClass(ProductForm))
22 |
23 | LayoutContent(LayoutContent.Props())(style := js.Dynamic.literal(padding = "50px"))(
24 | ProductDisplay(),
25 | hr(style := js.Dynamic.literal(margin = "30px")),
26 | Row(
27 | Col(Col.Props(span = 24))(
28 | h1("Insert a new product"),
29 | React.createElement(wrappedProductForm, js.Dictionary())
30 | )
31 | )
32 | )
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | Copyright (c) 2019 Rasnabit ltd
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy
4 | of this software and associated documentation files (the "Software"), to deal
5 | in the Software without restriction, including without limitation the rights
6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7 | copies of the Software, and to permit persons to whom the Software is
8 | furnished to do so, subject to the following conditions:
9 |
10 | The above copyright notice and this permission notice shall be included in all
11 | copies or substantial portions of the Software.
12 |
13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
14 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
15 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
16 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
17 | DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
18 | OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE
19 | OR OTHER DEALINGS IN THE SOFTWARE.
--------------------------------------------------------------------------------
/server/app/repositories/ProductRepo.scala:
--------------------------------------------------------------------------------
1 | package repositories
2 |
3 | import com.mypackage.Domain.Product
4 | import com.mypackage.ProductRepoLike
5 | import database.Tables
6 | import javax.inject.Inject
7 | import slick.jdbc.MySQLProfile.api._
8 |
9 | import scala.concurrent.ExecutionContext.Implicits.global
10 | import scala.concurrent.Future
11 |
12 | class ProductRepo @Inject()(db: Database) extends ProductRepoLike {
13 |
14 | def product(id: String): Future[Option[Product]] = {
15 | val action = Tables.Product.filter(_.id === id.toInt ).result
16 | db.run(action).map(_.headOption.map(p => Product(p.id.toString, p.name, p.description)))
17 | }
18 |
19 | def products: Future[Seq[Product]] = {
20 | val action = Tables.Product.result
21 | db.run(action).map(_.map(p => Product(p.id.toString, p.name, p.description)))
22 | }
23 |
24 | def addProduct(name: String, description: String): Future[Product] = {
25 | val action = Tables.Product.map(p => (p.name, p.description)) += ((name, description))
26 | db.run(action).map(id => Product(id.toString, name, description))
27 | }
28 | }
29 |
30 |
--------------------------------------------------------------------------------
/web_client/src/main/scala/reactrouter/NavLink.scala:
--------------------------------------------------------------------------------
1 | package reactrouter
2 |
3 | import slinky.core.ExternalComponent
4 | import slinky.core.ExternalComponentWithAttributes
5 | import slinky.web.html.a
6 |
7 | import scala.scalajs.js
8 | import scala.scalajs.js.UndefOr
9 | import scala.scalajs.js.annotation.JSImport
10 | import scala.scalajs.js.|
11 |
12 | @JSImport("react-router-dom", JSImport.Default)
13 | @js.native
14 | object ReactNavLink extends js.Object {
15 | val NavLink: js.Object = js.native
16 | }
17 |
18 | object NavLink extends ExternalComponentWithAttributes[a.tag.type] {
19 | case class Props(activeClassName: UndefOr[String] = js.undefined,
20 | activeStyle: UndefOr[js.Object] = js.undefined,
21 | exact: Boolean = false,
22 | strict: Boolean = false,
23 | isActive: UndefOr[js.Function2[js.Object, js.Object, Boolean]] = js.undefined,
24 | location: UndefOr[js.Object] = js.undefined,
25 | `aria-current`: String = "page",
26 | to: String | js.Object,
27 | replace: Boolean = false)
28 |
29 | override val component = ReactNavLink.NavLink
30 | }
31 |
--------------------------------------------------------------------------------
/server/app/Bootstrap.scala:
--------------------------------------------------------------------------------
1 | import play.api._
2 | import play.core.server.Server
3 | import play.core.server.ProdServerStart
4 | import play.core.server.RealServerProcess
5 | import play.core.server.ServerConfig
6 | import play.core.server.ServerProvider
7 |
8 | object Bootstrap {
9 | def main(args: Array[String]): Unit = if (args.isEmpty) {
10 | startServer(new RealServerProcess(args))
11 | } else {
12 | throw new IllegalStateException("Unable to parse arguments.")
13 | }
14 |
15 | def startServer(process: RealServerProcess): Server = {
16 | val config: ServerConfig = ProdServerStart.readServerConfigSettings(process)
17 | val application: Application = {
18 | val environment = Environment(config.rootDir, process.classLoader, Mode.Prod)
19 | val context = ApplicationLoader.Context.create(environment)
20 | val loader = ApplicationLoader(context)
21 | loader.load(context)
22 | }
23 | Play.start(application)
24 |
25 | val serverProvider: ServerProvider = ServerProvider.fromConfiguration(process.classLoader, config.configuration)
26 | val server = serverProvider.createServer(config, application)
27 | process.addShutdownHook(server.stop())
28 | server
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/project/Packaging.scala:
--------------------------------------------------------------------------------
1 | import com.typesafe.sbt.packager.Keys._
2 | import com.typesafe.sbt.packager.docker.DockerPlugin.autoImport.{Docker, dockerExposedPorts, dockerExposedVolumes}
3 | import com.typesafe.sbt.packager.universal.UniversalPlugin.autoImport.{Universal, useNativeZip}
4 | import sbt.Keys._
5 | import sbt._
6 |
7 | object Packaging {
8 | private[this] def isConf(x: (File, String)) = x._1.getAbsolutePath.contains("conf/")
9 |
10 | val settings = useNativeZip ++ Seq(
11 | topLevelDirectory in Universal := None,
12 | packageSummary := description.value,
13 | packageDescription := description.value,
14 |
15 | mappings in Universal := (mappings in Universal).value.filterNot(isConf),
16 |
17 | // Docker
18 | dockerExposedPorts := Seq(Shared.projectPort),
19 | dockerLabels ++= Map("project" -> Shared.projectId),
20 | dockerUpdateLatest := true,
21 | defaultLinuxInstallLocation in Docker := s"/opt/${Shared.projectId}",
22 | packageName in Docker := packageName.value,
23 | dockerExposedVolumes := Seq(s"/opt/${Shared.projectId}"),
24 | version in Docker := version.value,
25 | dockerUsername := Some(Shared.projectId),
26 |
27 | javaOptions in Universal ++= Seq("-J-Xmx2g", "-J-Xms256m", s"-Dproject=${Shared.projectId}")
28 | )
29 | }
--------------------------------------------------------------------------------
/project/PlayUtils.scala:
--------------------------------------------------------------------------------
1 | import java.io.Closeable
2 |
3 | import play.sbt.PlayNonBlockingInteractionMode
4 | import sbt.util.{Level, Logger}
5 |
6 | object PlayUtils {
7 | object NonBlockingInteractionMode extends PlayNonBlockingInteractionMode {
8 | object NullLogger extends Logger {
9 | def trace(t: => Throwable): Unit = ()
10 | def success(message: => String): Unit = ()
11 | def log(level: Level.Value, message: => String): Unit = ()
12 | }
13 |
14 | private var runningServers: Set[Closeable] = scala.collection.immutable.HashSet.empty
15 |
16 | override def start(server: => Closeable): Unit = synchronized {
17 | val theServer = server
18 | if (runningServers(theServer)) {
19 | println("Noop: This server was already started")
20 | } else {
21 | runningServers += theServer
22 | }
23 | }
24 |
25 | override def stop(): Unit = synchronized {
26 | if (runningServers.size > 1) {
27 | println("Stopping all servers")
28 | } else if (runningServers.size == 1) {
29 | println("Stopping server")
30 | } else {
31 | println("No running server to stop")
32 | }
33 | runningServers.foreach(_.close())
34 | runningServers = scala.collection.immutable.HashSet.empty
35 | }
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/web_client/src/main/scala/antd/Message.scala:
--------------------------------------------------------------------------------
1 | package antd
2 |
3 | import slinky.core.facade.ReactElement
4 |
5 | import scala.scalajs.js
6 | import scala.scalajs.js.annotation.JSImport
7 | import scala.scalajs.js.|
8 |
9 | @JSImport("antd", JSImport.Default)
10 | @js.native
11 | object AntMessage extends js.Object {
12 | val message: MessageInterface = js.native
13 | }
14 |
15 | @js.native
16 | trait MessageInterface extends js.Object {
17 | def success(content: String | ReactElement,
18 | duration: js.UndefOr[Double] = 1.5,
19 | onClose: js.UndefOr[js.Function0[js.Any]] = js.undefined): Unit = js.native
20 | def error(content: String | ReactElement,
21 | duration: js.UndefOr[Double] = 1.5,
22 | onClose: js.UndefOr[js.Function0[js.Any]] = js.undefined): Unit = js.native
23 | def info(content: String | ReactElement,
24 | duration: js.UndefOr[Double] = 1.5,
25 | onClose: js.UndefOr[js.Function0[js.Any]] = js.undefined): Unit = js.native
26 | def warning(content: String | ReactElement,
27 | duration: js.UndefOr[Double] = 1.5,
28 | onClose: js.UndefOr[js.Function0[js.Any]] = js.undefined): Unit = js.native
29 | def loading(content: String | ReactElement,
30 | duration: js.UndefOr[Double] = 1.5,
31 | onClose: js.UndefOr[js.Function0[js.Any]] = js.undefined): Unit = js.native
32 | }
33 |
34 |
--------------------------------------------------------------------------------
/project/plugins.sbt:
--------------------------------------------------------------------------------
1 | scalacOptions ++= Seq( "-unchecked", "-deprecation" )
2 |
3 | resolvers += Resolver.typesafeRepo("releases")
4 |
5 |
6 | resolvers += Resolver.url("jetbrains-bintray", url("http://dl.bintray.com/jetbrains/sbt-plugins/"))(Resolver.ivyStylePatterns)
7 |
8 |
9 | // The Play plugin
10 | addSbtPlugin("com.typesafe.play" % "sbt-plugin" % "2.7.0")
11 |
12 | // SBT-Web plugins
13 | addSbtPlugin("com.typesafe.sbt" % "sbt-web" % "1.4.4")
14 |
15 | libraryDependencies += "org.webjars.npm" % "source-map" % "0.7.3"
16 |
17 | // Scala.js
18 | addSbtPlugin("org.portable-scala" % "sbt-scalajs-crossproject" % "0.6.0")
19 |
20 | addSbtPlugin("org.scala-js" % "sbt-scalajs" % "0.6.26")
21 |
22 | addSbtPlugin("com.vmunier" % "sbt-web-scalajs" % "1.0.8-0.6" exclude("org.scala-js", "sbt-scalajs"))
23 |
24 | addSbtPlugin("ch.epfl.scala" % "sbt-scalajs-bundler" % "0.14.0")
25 |
26 | addSbtPlugin("ch.epfl.scala" % "sbt-web-scalajs-bundler" % "0.14.0")
27 |
28 | addSbtPlugin("com.typesafe.sbt" % "sbt-digest" % "1.1.3") // Checksum on sbt-web files
29 |
30 | addSbtPlugin("com.typesafe.sbt" % "sbt-gzip" % "1.0.2")
31 |
32 | // Dependency Resolution
33 | addSbtPlugin("io.get-coursier" % "sbt-coursier" % "1.0.0")
34 |
35 | // App Packaging
36 | addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "0.14.9")
37 |
38 | // Utilities
39 | addSbtPlugin("net.virtual-void" % "sbt-dependency-graph" % "0.9.0") // dependencyGraph
40 |
41 | addSbtPlugin("com.orrsella" % "sbt-stats" % "1.0.7") // stats
--------------------------------------------------------------------------------
/web_client/src/main/scala/antd/Button.scala:
--------------------------------------------------------------------------------
1 | package antd
2 |
3 | import org.scalajs.dom.Event
4 | import slinky.core.ExternalComponent
5 | import slinky.core.annotations.react
6 |
7 | import scala.scalajs.js
8 | import scala.scalajs.js.UndefOr
9 | import scala.scalajs.js.annotation.JSImport
10 | import scala.scalajs.js.|
11 |
12 | @JSImport("antd", JSImport.Default)
13 | @js.native
14 | object AntButton extends js.Object {
15 | val Button: js.Object = js.native
16 | }
17 |
18 | @js.native
19 | trait Delay extends js.Object {
20 | val delay: Int = js.native
21 | }
22 | object Delay {
23 | def apply(delay: Int) = js.Dynamic.literal(delay = delay).asInstanceOf[Delay]
24 | }
25 |
26 |
27 | @react object Button extends ExternalComponent {
28 | case class Props(disabled: Boolean = false,
29 | ghost: Boolean = false,
30 | href: UndefOr[String] = js.undefined,
31 | htmlType: String = "button",
32 | icon: UndefOr[String] = js.undefined,
33 | loading: Boolean | Delay = false,
34 | shape: UndefOr[String] = js.undefined,
35 | size: String = "default",
36 | target: UndefOr[String] = js.undefined,
37 | `type`: String = "default",
38 | onClick: UndefOr[Event => Unit] = js.undefined,
39 | block: Boolean = false)
40 |
41 | override val component = AntButton.Button
42 | }
43 |
44 |
45 |
46 |
--------------------------------------------------------------------------------
/server/conf/application.conf:
--------------------------------------------------------------------------------
1 | # https://www.playframework.com/documentation/latest/Configuration
2 |
3 | play.filters {
4 | headers.contentSecurityPolicy = "default-src 'self' 'unsafe-eval'; style-src 'self' 'unsafe-inline' cdn.jsdelivr.net; script-src 'self' 'unsafe-inline' 'unsafe-eval' cdn.jsdelivr.net"
5 |
6 | // Not suggested for production
7 | hosts.allowed = ["."]
8 |
9 | // Not suggested for production
10 | disabled += "play.filters.csrf.CSRFFilter"
11 | }
12 |
13 | // Not suggested for production
14 | play.filters.disabled += "play.filters.csrf.CSRFFilter"
15 |
16 | // Dependency Injection module
17 | play.modules.enabled += "di.DiModule"
18 |
19 | slick.dbs {
20 | postgres {
21 | profile="slick.jdbc.PostgresProfile$"
22 | db {
23 | driver = "org.postgresql.Driver"
24 | url = "jdbc:postgresql://localhost:5432/test"
25 | databaseName = "test"
26 | user = "admin"
27 | password = "admin"
28 | }
29 | codegen {
30 | package = "database"
31 | outputDir = "slick"
32 | }
33 | }
34 | mysql {
35 | profile="slick.jdbc.MySQLProfile$"
36 | db {
37 | driver="com.mysql.cj.jdbc.Driver"
38 | url = "jdbc:mysql://localhost/test?nullNamePatternMatchesAll=true&useSSL=false"
39 | databaseName = "test"
40 | user = "admin"
41 | password = "admin"
42 | }
43 | codegen {
44 | package = "database"
45 | outputDir = "slick"
46 | }
47 | }
48 | default = ${slick.dbs.mysql}
49 | }
50 |
--------------------------------------------------------------------------------
/web_client/src/main/scala/antd/Input.scala:
--------------------------------------------------------------------------------
1 | package antd
2 |
3 | import org.scalajs.dom.Event
4 | import slinky.core.ExternalComponentWithAttributes
5 | import slinky.core.annotations.react
6 | import slinky.core.facade.ReactElement
7 | import slinky.web.html.input
8 |
9 | import scala.scalajs.js
10 | import scala.scalajs.js.UndefOr
11 | import scala.scalajs.js.annotation.JSImport
12 | import scala.scalajs.js.|
13 |
14 | @JSImport("antd", JSImport.Default)
15 | @js.native
16 | object AntInput extends js.Object {
17 | val Input: js.Object = js.native
18 | }
19 |
20 | @react object Input extends ExternalComponentWithAttributes[input.tag.type] {
21 | case class Props(addonAfter: UndefOr[String | ReactElement] = js.undefined,
22 | addonBefore: UndefOr[String | ReactElement] = js.undefined,
23 | defaultValue: UndefOr[String] = js.undefined,
24 | disabled: Boolean = false,
25 | id: UndefOr[String] = js.undefined,
26 | prefix: UndefOr[String | ReactElement] = js.undefined,
27 | size: String = "default",
28 | suffix: UndefOr[String | ReactElement] = js.undefined,
29 | `type`: String = "text",
30 | value: UndefOr[String] = js.undefined,
31 | onChange: UndefOr[Event => Unit] = js.undefined,
32 | onPressEnter: UndefOr[Event => Unit] = js.undefined,
33 | allowClear: Boolean = false
34 | )
35 |
36 | override val component = AntInput.Input
37 | }
38 |
39 |
40 |
41 |
--------------------------------------------------------------------------------
/web_client/src/main/scala/reactrouter/Route.scala:
--------------------------------------------------------------------------------
1 | package reactrouter
2 |
3 | import slinky.core.ExternalComponent
4 | import slinky.core.ReactComponentClass
5 | import slinky.core.annotations.react
6 | import slinky.core.facade.ReactElement
7 |
8 | import scala.scalajs.js
9 | import scala.scalajs.js.UndefOr
10 | import scala.scalajs.js.annotation.JSImport
11 | import scala.scalajs.js.|
12 |
13 | @JSImport("react-router-dom", JSImport.Default)
14 | @js.native
15 | object ReactRoute extends js.Object {
16 | val Route: js.Object = js.native
17 | }
18 |
19 | @js.native
20 | trait RouteProps extends js.Object {
21 | val `match`: js.Object = js.native
22 | val location: js.Object = js.native
23 | val history: History = js.native
24 | }
25 |
26 | @js.native
27 | trait History extends js.Object {
28 | def push(path: String, state: js.UndefOr[js.Object] = js.undefined): Unit = js.native
29 | def replace(path: String, state: js.UndefOr[js.Object] = js.undefined): Unit = js.native
30 | }
31 |
32 | @react object Route extends ExternalComponent {
33 | case class Props(component: UndefOr[ReactComponentClass[_]] = js.undefined,
34 | render: UndefOr[js.Function1[RouteProps, ReactElement]] = js.undefined,
35 | // children -> TODO
36 | path: String | js.Array[String] = "/",
37 | exact: Boolean = false,
38 | strict: Boolean = false,
39 | location: UndefOr[js.Object] = js.undefined,
40 | sensitive: Boolean = false)
41 |
42 | override val component = ReactRoute.Route
43 | }
44 |
--------------------------------------------------------------------------------
/server/app/infrastructure/ApplyEvolutions.scala:
--------------------------------------------------------------------------------
1 | package infrastructure
2 |
3 | import java.io.File
4 |
5 | import com.typesafe.config.ConfigFactory
6 | import play.api.Configuration
7 | import play.api.Environment
8 | import play.api.Mode
9 | import play.api.db.evolutions.OfflineEvolutions
10 | import play.api.db.slick.DefaultSlickApi
11 | import play.api.db.slick.evolutions.SlickDBApi
12 | import play.api.inject.DefaultApplicationLifecycle
13 | import scala.concurrent.ExecutionContext.Implicits.global
14 |
15 | /**
16 | * This class applies Evolutions without the need to start the play application.
17 | *
18 | * It is used because the first time you check-out the code and you compile it, the
19 | * database is empty and therefore no slick code generation can be done. An empty
20 | * database leads to a empty code generation and therefore compilation errors.
21 | *
22 | * This is run before the code generation so that the database is initialized with
23 | * the evolutions and the code generation can take place.
24 | */
25 | object ApplyEvolutions {
26 |
27 | def main(argv: Array[String]) = {
28 | val baseDir = argv(0)
29 | val config = ConfigFactory.load("application.conf")
30 | val environment = Environment(new File(baseDir), this.getClass.getClassLoader, Mode.Dev)
31 |
32 | val dbApi = SlickDBApi(new DefaultSlickApi(
33 | environment,
34 | Configuration(config),
35 | new DefaultApplicationLifecycle
36 | ))
37 |
38 | OfflineEvolutions.applyScript(
39 | new File("."),
40 | this.getClass.getClassLoader,
41 | dbApi,
42 | dbName = "default"
43 | )
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/server/conf/logback.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | ${application.home:-.}/logs/application.log
8 |
9 | %date [%level] from %logger in %thread - %message%n%xException
10 |
11 |
12 |
13 |
14 |
15 | %coloredLevel %logger{15} - %message%n%xException{10}
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
--------------------------------------------------------------------------------
/web_client/src/main/scala/com/mypackage/App.scala:
--------------------------------------------------------------------------------
1 | package com.mypackage
2 |
3 | import antd.AntForm
4 | import reactrouter.BrowserRouter
5 | import reactrouter.Redirect
6 | import reactrouter.Route
7 | import slinky.core.Component
8 | import slinky.core.StatelessComponent
9 | import slinky.core.annotations.react
10 | import slinky.core.ReactComponentClass._
11 | import slinky.core.facade.ReactElement
12 | import slinky.web.html.h1
13 | import authentication.Login
14 | import org.scalajs.dom.ext.LocalStorage
15 | import reactrouter.RouteProps
16 | import slinky.core.facade.React
17 | import slinky.web.svg.path
18 |
19 | import scala.scalajs.js
20 |
21 | @react class App extends Component {
22 | type Props = Unit
23 |
24 | case class State(isLoggingIn: Boolean, isLoggedIn: Boolean)
25 |
26 | override def initialState: State = State(false, false)
27 |
28 | val loginComponent: js.Function1[RouteProps, ReactElement] = routeProps => {
29 | val wrappedLoginPage = AntForm.Form.create()(wrapperToClass(Login))
30 | React.createElement(wrappedLoginPage, js.Dictionary("history" -> routeProps.history))
31 | }
32 | val rootPath: js.Function1[RouteProps, ReactElement] = p => Redirect(Redirect.Props(to = "/app"))
33 | val renderAppIfLoggedIn: js.Function1[RouteProps, ReactElement] = p =>
34 | if (LocalStorage("bearerToken").isEmpty)
35 | Redirect(Redirect.Props(to = "/login"))
36 | else
37 | MainPage()
38 |
39 |
40 | def render() = {
41 |
42 | BrowserRouter(
43 | Route(Route.Props(path = "/login", exact = true, render = loginComponent)),
44 | Route(Route.Props(path = "/app", render = renderAppIfLoggedIn)),
45 | Route(Route.Props(path = "/", exact = true, render = rootPath))
46 | )
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/web_client/webpack/webpack-core.config.js:
--------------------------------------------------------------------------------
1 | var path = require("path");
2 | var CopyWebpackPlugin = require('copy-webpack-plugin');
3 | var CleanWebpackPlugin = require('clean-webpack-plugin');
4 | var webpack = require('webpack');
5 |
6 | var pathsToClean = ['dist'];
7 | var cleanOptions = {
8 | verbose: true
9 | };
10 |
11 | module.exports = {
12 | mode: "development",
13 | resolve: {
14 | alias: {
15 | "resources": path.resolve(__dirname, "../../../../src/main/resources"),
16 | "scalajs": path.resolve(__dirname, "./scalajs-entry.js")
17 | },
18 | modules: [ path.resolve(__dirname, 'node_modules') ]
19 | },
20 | module: {
21 | rules: [
22 | // Config coming from HMR webpack
23 | {
24 | test: /\.css$/,
25 | use: [ 'style-loader', 'css-loader' ]
26 | },
27 | // "file" loader for svg
28 | {
29 | test: /\.svg\$/,
30 | use: [
31 | {
32 | loader: 'file-loader',
33 | query: {
34 | name: 'static/media/[name].[hash:8].[ext]'
35 | }
36 | }
37 | ]
38 | }
39 | ]
40 | },
41 | plugins: [
42 | new CleanWebpackPlugin(pathsToClean, cleanOptions),
43 | new CopyWebpackPlugin([
44 | { from: path.resolve(__dirname, "../../../../public") }
45 | ]),
46 | new webpack.HotModuleReplacementPlugin()
47 | ],
48 | output: {
49 | devtoolModuleFilenameTemplate: (f) => {
50 | if (f.resourcePath.startsWith("http://") ||
51 | f.resourcePath.startsWith("https://") ||
52 | f.resourcePath.startsWith("file://")) {
53 | return f.resourcePath;
54 | } else {
55 | return "webpack://" + f.namespace + "/" + f.resourcePath;
56 | }
57 | }
58 | }
59 | }
--------------------------------------------------------------------------------
/server/conf/evolutions/default/1.sql:
--------------------------------------------------------------------------------
1 | -- !Ups
2 |
3 | CREATE TABLE product (
4 | id int NOT NULL AUTO_INCREMENT,
5 | name varchar(255) NOT NULL,
6 | description varchar(255) NOT NULL,
7 | PRIMARY KEY (id)
8 | );
9 |
10 | CREATE TABLE picture (
11 | id int NOT NULL AUTO_INCREMENT,
12 | productId int NOT NULL,
13 | width int NOT NULL,
14 | height int NOT NULL,
15 | url varchar(255),
16 | PRIMARY KEY (id)
17 | );
18 |
19 | INSERT INTO picture (productId, width, height, url) VALUES
20 | (1, 50, 50, '/assets/images/cheesecake1.jpeg'),
21 | (1, 50, 50, '/assets/images/cheesecake2.jpeg'),
22 | (2, 60, 30, '/assets/images/nutella.jpeg');
23 |
24 | INSERT INTO product (name, description) VALUES
25 | ('Cheesecake', 'Tasty'),
26 | ('Health Potion', '+50 HP');
27 |
28 | CREATE TABLE loginInfo (
29 | id varchar(255) NOT NULL,
30 | hasher varchar(255) NOT NULL,
31 | password varchar(255) NOT NULL,
32 | salt varchar(255),
33 | PRIMARY KEY (id)
34 | );
35 |
36 | --
37 | -- This record adds by default the user with credentials:
38 | -- username: admin
39 | -- password: admin
40 | -- should not be kept in production
41 | --
42 | INSERT INTO loginInfo (id, hasher, password) values
43 | ('credentials:admin', 'bcrypt', '$2a$10$QpRYc9HiCvQ2FKhe7SO8vuyy9ZJInVJAj6i3f5l1z9gYzjh9pNMEy');
44 |
45 | CREATE TABLE userSession(
46 | id varchar(256) NOT NULL,
47 | loginInfoId varchar(255) NOT NULL,
48 | loginInfoKey varchar(255) NOT NULL,
49 | lastUsed datetime NOT NULL,
50 | expiration datetime NOT NULL,
51 | idleTimeout int,
52 | creation datetime DEFAULT CURRENT_TIMESTAMP,
53 | PRIMARY KEY (id)
54 | );
55 |
56 | -- !Downs
57 |
58 | DROP TABLE userSession;
59 | DROP TABLE loginInfo;
60 | DROP TABLE picture;
61 | DROP TABLE product;
62 |
--------------------------------------------------------------------------------
/project/WebpackUtils.scala:
--------------------------------------------------------------------------------
1 | import org.scalajs.sbtplugin.ScalaJSPlugin.autoImport._
2 | import sbt.Keys._
3 | import sbt._
4 | import scalajsbundler.sbtplugin.ScalaJSBundlerPlugin.autoImport._
5 |
6 | object WebpackUtils {
7 |
8 | val deleteWebpackCache = taskKey[Unit]("Deletes the cache used by webpack if there are changes to the monitored files")
9 |
10 | val wiringTasks = Seq(
11 | deleteWebpackCache := {
12 | val webpackMonitored = (Compile / fastOptJS / webpackMonitoredFiles).value
13 | val webpackCacheDir = (Compile / fastOptJS / webpack / streams).value.cacheDirectory
14 | val cacheDirectory = streams.value.cacheDirectory / "webpack-cache"
15 | val logger = streams.value.log
16 |
17 | /*
18 | * Only deletes the cache if a monitored file has changed.
19 | *
20 | * This has been done because webpack does not monitor the output file
21 | * created by fastOptJS (i.e. web_client-fastopt.js). So when the output file
22 | * changes, in LibraryTasks.bundle no bundle is created because all the
23 | * rest of the monitored files stay the same.
24 | */
25 | val actionToPerform = FileFunction.cached(cacheDirectory, inStyle = FileInfo.hash) {_ =>
26 | // Deleting cache directory to make webpack run every time
27 | // TODO: Improve with webpackDevServer
28 | val dirToDelete = webpackCacheDir / "fastOptJS-webpack-libraries"
29 | logger.info("Deleting " + dirToDelete)
30 | IO.delete(dirToDelete)
31 | Set()
32 | }
33 |
34 | actionToPerform(webpackMonitored.toSet)
35 |
36 | },
37 | (Compile / fastOptJS / webpack) := {
38 | streams.value.log.info("Running webpack")
39 | deleteWebpackCache.value
40 | (Compile / fastOptJS / webpack).value
41 | }
42 | )
43 |
44 | }
45 |
--------------------------------------------------------------------------------
/web_client/src/main/scala/com/mypackage/product/ProductDisplay.scala:
--------------------------------------------------------------------------------
1 | package com.mypackage.product
2 |
3 | import antd.Card
4 | import antd.CardMeta
5 | import antd.Col
6 | import antd.Row
7 | import com.apollographql.scalajs.react.Query
8 | import com.mypackage.AllProductsQuery
9 | import slinky.core.StatelessComponent
10 | import slinky.core.annotations.react
11 | import slinky.web.html.div
12 | import slinky.web.html.h1
13 | import slinky.web.html.img
14 | import slinky.web.html.span
15 | import slinky.web.html.src
16 | import slinky.web.html.style
17 | import slinky.core.WithAttrs._
18 | import slinky.core.BuildingComponent._
19 | import slinky.core.TagMod
20 |
21 | import scala.scalajs.js
22 |
23 | @react class ProductDisplay extends StatelessComponent {
24 | type Props = Unit
25 |
26 | override def render() =
27 | Query(AllProductsQuery) { result =>
28 | if (result.loading) {
29 | h1("Loading!")
30 | } else if (result.error.isDefined) {
31 | h1("Error: " + result.error.get.message)
32 | } else {
33 | div(
34 | h1("Products display"),
35 | Row(Row.Props(gutter = 16, justify = "space-around", align = "middle"))(
36 | result.data.get.products.map { product =>
37 | Col(Col.Props(span = 6))(
38 | renderProductCard(product)
39 | ): TagMod[Object]
40 | }: _*
41 | )
42 | )
43 | }
44 | }
45 |
46 | private def renderProductCard(product: AllProductsQuery.Data.Product) =
47 | Card(Card.Props(
48 | cover = img(src := product.pictures.headOption.flatMap(_.url).getOrElse("/assets/images/default_item.jpg"))()
49 | ))(
50 | style := js.Dynamic.literal(maxWidth = "240px"),
51 | CardMeta(
52 | CardMeta.Props(title = span(product.name)(), description = span(product.description)())
53 | )
54 | )
55 | }
56 |
--------------------------------------------------------------------------------
/web_client/src/main/scala/antd/Grid.scala:
--------------------------------------------------------------------------------
1 | package antd
2 |
3 | import slinky.core.ExternalComponent
4 | import slinky.core.annotations.react
5 | import slinky.core.facade.ReactElement
6 |
7 | import scala.scalajs.js
8 | import scala.scalajs.js.UndefOr
9 | import scala.scalajs.js.annotation.JSImport
10 | import scala.scalajs.js.|
11 |
12 | @JSImport("antd", JSImport.Default)
13 | @js.native
14 | object AntGrid extends js.Object {
15 | val Row: js.Object = js.native
16 | val Col: js.Object = js.native
17 | }
18 |
19 | case class Gutter(xs: Int = 0,
20 | sm: Int = 0,
21 | md: Int = 0,
22 | lg: Int = 0,
23 | xl: Int = 0,
24 | xxl: Int = 0)
25 |
26 | case class ColProps(offset: Int = 0,
27 | order: Int = 0,
28 | pull: Int = 0,
29 | push: Int = 0,
30 | span: Int = 0)
31 |
32 | @react object Row extends ExternalComponent {
33 | case class Props(align: String = "top",
34 | gutter: Int | Gutter = 0,
35 | justify: String = "start",
36 | `type`: UndefOr[String] = js.undefined)
37 |
38 | override val component = AntGrid.Row
39 | }
40 |
41 | @react object Col extends ExternalComponent {
42 | case class Props(offset: Int = 0,
43 | order: Int = 0,
44 | pull: Int = 0,
45 | push: Int = 0,
46 | span: Int = 0,
47 | xs: UndefOr[Int | Gutter] = js.undefined,
48 | sm: UndefOr[Int | Gutter] = js.undefined,
49 | md: UndefOr[Int | Gutter] = js.undefined,
50 | lg: UndefOr[Int | Gutter] = js.undefined,
51 | xl: UndefOr[Int | Gutter] = js.undefined,
52 | xxl: UndefOr[Int | Gutter] = js.undefined)
53 |
54 | override val component = AntGrid.Col
55 | }
56 |
57 |
--------------------------------------------------------------------------------
/schema/src/main/scala/com/mypackage/GraphQLSchema.scala:
--------------------------------------------------------------------------------
1 | package com.mypackage
2 |
3 | import sangria.macros.derive._
4 | import sangria.schema._
5 | import com.mypackage.Domain._
6 |
7 | object GraphQLSchema {
8 |
9 | val IdentifiableType = InterfaceType(
10 | "Identifiable",
11 | "Entity that can be identified",
12 |
13 | fields[RequestContext, Identifiable](
14 | Field("id", StringType, resolve = _.value.id)))
15 |
16 | val PictureType =
17 | deriveObjectType[Unit, Picture](
18 | ObjectTypeDescription("The product picture"),
19 | DocumentField("url", "Picture CDN URL"))
20 |
21 | val ProductType =
22 | deriveObjectType[RequestContext, Product](
23 | Interfaces(IdentifiableType),
24 | AddFields(
25 | Field("pictures", ListType(PictureType),
26 | arguments = Argument("size", IntType) :: Nil,
27 | resolve = c => c.ctx.pictureRepo.picturesByProduct(c.value.id))
28 | )
29 | )
30 |
31 | val Id = Argument("id", StringType)
32 |
33 | val QueryType = ObjectType("Query", fields[RequestContext, Any](
34 | Field("product", OptionType(ProductType),
35 | description = Some("Returns a product with specific `id`."),
36 | arguments = Id :: Nil,
37 | resolve = c ⇒ c.ctx.productRepo.product(c arg Id)),
38 |
39 | Field("products", ListType(ProductType),
40 | description = Some("Returns a list of all available products."),
41 | resolve = _.ctx.productRepo.products)))
42 |
43 | val NameArg = Argument("name", StringType)
44 | val DescriptionArg = Argument("description", StringType)
45 |
46 | val MutationType = ObjectType("Mutation",
47 | fields[RequestContext, Any](
48 | Field("addProduct", ProductType,
49 | arguments = NameArg :: DescriptionArg :: Nil,
50 | resolve = req => req.ctx.productRepo.addProduct(req.arg(NameArg), req.arg(DescriptionArg))
51 | )
52 | )
53 | )
54 |
55 | val schema = Schema(QueryType, Some(MutationType))
56 |
57 | }
58 |
--------------------------------------------------------------------------------
/web_client/src/main/scala/com/mypackage/MainPage.scala:
--------------------------------------------------------------------------------
1 | package com.mypackage
2 |
3 | import antd.Layout
4 | import antd.LayoutFooter
5 | import antd.LayoutHeader
6 | import antd.LayoutSider
7 | import antd.Menu
8 | import antd.MenuItem
9 | import reactrouter.BrowserRouter
10 | import reactrouter.Route
11 | import slinky.core.ReactComponentClass._
12 | import slinky.core._
13 | import slinky.core.annotations.react
14 | import slinky.core.facade.ReactElement
15 | import slinky.web.html._
16 | import product.Products
17 | import reactrouter.NavLink
18 | import reactrouter.RouteProps
19 | import version.util.Version
20 |
21 | import scala.scalajs.js
22 | import scala.scalajs.js.annotation.JSImport
23 |
24 | @JSImport("resources/MainPage.css", JSImport.Default)
25 | @js.native
26 | object MainPageCSS extends js.Object
27 |
28 | @react class MainPage extends StatelessComponent {
29 | type Props = Unit
30 |
31 | private val css = MainPageCSS
32 |
33 | def render() = {
34 | Layout(Layout.Props(className = "main-layout"))(
35 | renderHeader(),
36 | renderContent(),
37 | LayoutFooter(
38 | span(s"Version ${Version.version}")
39 | )
40 | )
41 | }
42 |
43 | private def renderContent() = {
44 | val testComponent: js.Function1[RouteProps, ReactElement] = p => h1("Test")
45 | Layout(
46 | renderSider(),
47 | Layout(
48 | Route(Route.Props(path = "/app", exact = true, component = wrapperToClass(Products))),
49 | Route(Route.Props(path = "/app/test", render = testComponent))
50 | )
51 | )
52 | }
53 |
54 | private def renderHeader() =
55 | LayoutHeader(
56 | h1(className := "app-title")("Welcome to React (with Scala.js!) on Play")
57 | )
58 |
59 |
60 | private def renderSider() = {
61 | LayoutSider(
62 | Menu(
63 | MenuItem(
64 | NavLink(NavLink.Props(to = "/app"))("Products")
65 | ),
66 | MenuItem(
67 | NavLink(NavLink.Props(to = "/app/test"))("Test")
68 | )
69 | )
70 | )
71 | }
72 | }
73 |
74 |
--------------------------------------------------------------------------------
/server/app/auth/UserSessionRepository.scala:
--------------------------------------------------------------------------------
1 | package auth
2 |
3 | import com.github.tototoshi.slick.MySQLJodaSupport._
4 | import com.mohiva.play.silhouette.api.LoginInfo
5 | import com.mohiva.play.silhouette.api.repositories.AuthenticatorRepository
6 | import com.mohiva.play.silhouette.impl.authenticators.BearerTokenAuthenticator
7 | import database.Tables
8 | import javax.inject.Inject
9 | import slick.jdbc.MySQLProfile.api._
10 |
11 | import scala.concurrent.ExecutionContext.Implicits.global
12 | import scala.concurrent.Future
13 | import scala.concurrent.duration._
14 | import scala.language.implicitConversions
15 |
16 | class UserSessionRepository @Inject()(db: Database) extends AuthenticatorRepository[BearerTokenAuthenticator] {
17 |
18 | override def find(id: String): Future[Option[BearerTokenAuthenticator]] = {
19 | val query = Tables.Usersession.filter(_.id === id).result.headOption
20 | db.run(query).map(_.map { a =>
21 | val loginInfo = LoginInfo(a.logininfoid, a.logininfokey)
22 | BearerTokenAuthenticator(a.id, loginInfo, a.lastused, a.expiration, a.idletimeout.map(secondsToDuration))
23 | })
24 | }
25 |
26 | override def add(a: BearerTokenAuthenticator): Future[BearerTokenAuthenticator] = {
27 | val query = Tables.Usersession.map(us => (us.id, us.logininfoid, us.logininfokey, us.lastused, us.expiration, us.idletimeout)) += (
28 | (a.id, a.loginInfo.providerID, a.loginInfo.providerKey,
29 | a.lastUsedDateTime, a.expirationDateTime, a.idleTimeout.map(durationToSeconds))
30 | )
31 | db.run(query).map(_ => a)
32 | }
33 |
34 | override def update(a: BearerTokenAuthenticator): Future[BearerTokenAuthenticator] = {
35 | val query = Tables.Usersession.filter(_.id === a.id)
36 | .map(us => (us.logininfoid, us.logininfokey, us.lastused, us.expiration, us.idletimeout))
37 | .update((a.loginInfo.providerID, a.loginInfo.providerKey,
38 | a.lastUsedDateTime, a.expirationDateTime, a.idleTimeout.map(durationToSeconds)))
39 | db.run(query).map(_ => a)
40 | }
41 |
42 | override def remove(id: String): Future[Unit] = {
43 | val query = Tables.Usersession.filter(_.id === id).delete
44 | db.run(query).map(_ => ())
45 | }
46 |
47 | private def secondsToDuration(s: Int): FiniteDuration = s seconds
48 | private def durationToSeconds(f: FiniteDuration): Int = f.toSeconds.toInt
49 |
50 | }
51 |
--------------------------------------------------------------------------------
/server/app/controllers/AuthenticationController.scala:
--------------------------------------------------------------------------------
1 | package controllers
2 |
3 | import auth.AuthEnvironment
4 | import auth.UserIdentityService
5 | import com.mohiva.play.silhouette.api.Environment
6 | import com.mohiva.play.silhouette.api.util.Credentials
7 | import com.mohiva.play.silhouette.impl.exceptions.IdentityNotFoundException
8 | import com.mohiva.play.silhouette.impl.providers.CredentialsProvider
9 | import javax.inject.Inject
10 | import play.Logger
11 | import play.api.libs.json.Json
12 | import play.api.libs.json.Reads
13 | import play.api.mvc.AbstractController
14 | import play.api.mvc.ControllerComponents
15 |
16 | import scala.concurrent.ExecutionContext.Implicits.global
17 | import scala.concurrent.Await
18 | import scala.concurrent.Future
19 | import scala.util.Try
20 |
21 | class AuthenticationController @Inject()(cc: ControllerComponents,
22 | env: Environment[AuthEnvironment],
23 | credentialsProvider: CredentialsProvider,
24 | userIdentityService: UserIdentityService) extends AbstractController(cc) {
25 |
26 | def authenticate = Action.async(parse.json) { implicit request =>
27 | request.body.validate[AuthCredentials].fold(
28 | errors => Future.successful(BadRequest(errors.mkString(","))),
29 | reqCredentials => {
30 | val credentials = Credentials(reqCredentials.username, reqCredentials.password)
31 | val bearerAuthenticatorToken = for {
32 | loginInfo <- credentialsProvider.authenticate(credentials)
33 | maybeUser <- userIdentityService.retrieve(loginInfo)
34 | authenticator <- maybeUser match {
35 | case None => Future.failed(new IdentityNotFoundException("Couldn't authenticate the user"))
36 | case Some(user) => env.authenticatorService.create(loginInfo)
37 | }
38 | token <- env.authenticatorService.init(authenticator)
39 | } yield token
40 |
41 | bearerAuthenticatorToken.map { token =>
42 | Ok(s"$token")
43 | } recover {
44 | case t => Unauthorized("Unable to login")
45 | }
46 | }
47 | )
48 | }
49 |
50 | }
51 |
52 | case class AuthCredentials(username: String,
53 | password: String)
54 | object AuthCredentials {
55 | implicit val fmt: Reads[AuthCredentials] = Json.reads[AuthCredentials]
56 | }
57 |
--------------------------------------------------------------------------------
/server/app/auth/PasswordInfoRepository.scala:
--------------------------------------------------------------------------------
1 | package auth
2 |
3 | import com.google.inject.Inject
4 | import com.mohiva.play.silhouette.api.LoginInfo
5 | import com.mohiva.play.silhouette.api.util.PasswordInfo
6 | import com.mohiva.play.silhouette.persistence.daos.DelegableAuthInfoDAO
7 | import database.Tables
8 | import slick.jdbc.MySQLProfile.api._
9 |
10 | import scala.concurrent.ExecutionContext.Implicits.global
11 | import scala.concurrent.Future
12 |
13 | class PasswordInfoRepository @Inject()(db: Database) extends DelegableAuthInfoDAO[PasswordInfo] {
14 |
15 | override def find(loginInfo: LoginInfo): Future[Option[PasswordInfo]] = {
16 | val query = loginInfoById(loginInfo).result
17 | db.run(query).map(_.headOption.map(pi => PasswordInfo(pi.hasher, pi.password, pi.salt)))
18 | }
19 |
20 |
21 | override def add(loginInfo: LoginInfo, authInfo: PasswordInfo): Future[PasswordInfo] =
22 | runAndReturn(addQuery(loginInfo, authInfo), authInfo)
23 |
24 | private def addQuery = (loginInfo: LoginInfo, authInfo: PasswordInfo) =>
25 | Tables.Logininfo.map(li =>
26 | (li.id, li.hasher, li.password, li.salt)
27 | ) += ((idFromLoginInfo(loginInfo), authInfo.hasher, authInfo.password, authInfo.salt))
28 |
29 |
30 | override def update(loginInfo: LoginInfo, authInfo: PasswordInfo): Future[PasswordInfo] =
31 | runAndReturn(updateQuery(loginInfo, authInfo), authInfo)
32 |
33 | private def updateQuery = (loginInfo: LoginInfo, authInfo: PasswordInfo) =>
34 | loginInfoById(loginInfo)
35 | .map(li => (li.hasher, li.password, li.salt))
36 | .update((authInfo.hasher, authInfo.password, authInfo.salt))
37 |
38 | override def save(loginInfo: LoginInfo, authInfo: PasswordInfo): Future[PasswordInfo] = {
39 | val query = for {
40 | isLoginInfoPresent <- loginInfoById(loginInfo).exists.result
41 | queryToUse = if (isLoginInfoPresent) updateQuery else addQuery
42 | action <- queryToUse(loginInfo, authInfo)
43 | } yield action
44 |
45 | runAndReturn(query, authInfo)
46 | }
47 |
48 | override def remove(loginInfo: LoginInfo): Future[Unit] = runAndReturn(loginInfoById(loginInfo).delete, ())
49 |
50 | private def idFromLoginInfo(loginInfo: LoginInfo) = loginInfo.providerID + ":" + loginInfo.providerKey
51 | private def loginInfoById(loginInfo: LoginInfo) = Tables.Logininfo.filter(_.id === idFromLoginInfo(loginInfo))
52 | private def runAndReturn[T](action: DBIOAction[Int, NoStream, Nothing], ret: T) = db.run(action).map(_ => ret)
53 | }
54 |
--------------------------------------------------------------------------------
/server/app/controllers/GraphQLController.scala:
--------------------------------------------------------------------------------
1 | package controllers
2 |
3 | import auth.AuthEnvironment
4 | import com.google.inject.Inject
5 | import com.mohiva.play.silhouette.api.Silhouette
6 | import com.mypackage.GraphQLSchema._
7 | import com.mypackage.RequestContext
8 | import play.api.libs.json._
9 | import play.api.mvc.AbstractController
10 | import play.api.mvc.ControllerComponents
11 | import play.twirl.api.Html
12 | import sangria.ast.Document
13 | import sangria.execution.ErrorWithResolver
14 | import sangria.execution.Executor
15 | import sangria.execution.QueryAnalysisError
16 | import sangria.marshalling.playJson._
17 | import sangria.parser.QueryParser
18 |
19 | import scala.concurrent.Future
20 | import scala.util.Failure
21 | import scala.util.Success
22 |
23 | class GraphQLController @Inject()(cc: ControllerComponents,
24 | e: Silhouette[AuthEnvironment],
25 | requestContext: RequestContext) extends AbstractController(cc) {
26 |
27 | implicit val execution = cc.executionContext
28 |
29 | def graphql = e.SecuredAction.async(parse.json) { request ⇒
30 | def parseVariables(variables: String) =
31 | if (variables.trim == "" || variables.trim == "null") Json.obj() else Json.parse(variables).as[JsObject]
32 |
33 | val query = (request.body \ "query").as[String]
34 | val operation = (request.body \ "operationName").asOpt[String]
35 | val variables = (request.body \ "variables").toOption.flatMap {
36 | case JsString(vars) ⇒ Some(parseVariables(vars))
37 | case obj: JsObject ⇒ Some(obj)
38 | case _ ⇒ None
39 | }
40 |
41 | QueryParser.parse(query) match {
42 | // query parsed successfully, time to execute it!
43 | case Success(queryAst) ⇒
44 | executeGraphQLQuery(queryAst, operation, variables.getOrElse(Json.obj()))
45 |
46 | // can't parse GraphQLController query, return error
47 | case Failure(error) ⇒
48 | Future.successful(BadRequest(Json.obj("error" → error.getMessage)))
49 | }
50 | }
51 |
52 | def executeGraphQLQuery(query: Document, op: Option[String], vars: JsObject) =
53 | Executor.execute(schema, query, requestContext, operationName = op, variables = vars)
54 | .map(Ok(_))
55 | .recover {
56 | case error: QueryAnalysisError ⇒ BadRequest(error.resolveError)
57 | case error: ErrorWithResolver ⇒ InternalServerError(error.resolveError)
58 | }
59 |
60 | def graphiQL = e.UnsecuredAction {
61 | Html
62 | Ok(views.html.graphiql())
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/web_client/src/main/scala/antd/Card.scala:
--------------------------------------------------------------------------------
1 | package antd
2 |
3 | import slinky.core.ExternalComponent
4 | import slinky.core.annotations.react
5 | import slinky.core.facade.ReactElement
6 |
7 | import scala.scalajs.js
8 | import scala.scalajs.js.UndefOr
9 | import scala.scalajs.js.annotation.JSImport
10 | import scala.scalajs.js.|
11 |
12 | @JSImport("antd", JSImport.Default)
13 | @js.native
14 | object AntCard extends js.Object {
15 | val Card: AntCardObj = js.native
16 | }
17 |
18 | @js.native
19 | trait AntCardObj extends js.Object {
20 | val Meta: js.Object = js.native
21 | val Grid: js.Object = js.native
22 | }
23 |
24 | @react object Card extends ExternalComponent {
25 | case class TabPaneHead(key: String, tab: ReactElement)
26 |
27 | case class Props(actions: UndefOr[Seq[ReactElement]] = js.undefined,
28 | activeTabKey: UndefOr[String] = js.undefined,
29 | headStyle: UndefOr[js.Object] = js.undefined,
30 | bodyStyle: UndefOr[js.Object] = js.undefined,
31 | bordered: Boolean = true,
32 | cover: UndefOr[ReactElement] = js.undefined,
33 | defaultActiveTabKey: UndefOr[String] = js.undefined,
34 | extra: UndefOr[String | ReactElement] = js.undefined,
35 | hoverable: Boolean = false,
36 | loading: Boolean = false,
37 | tabList: UndefOr[Seq[TabPaneHead]] = js.undefined,
38 | size: String = "default",
39 | title: UndefOr[String | ReactElement] = js.undefined,
40 | `type`: UndefOr[String] = js.undefined,
41 | onTabChange: UndefOr[String => Unit] = js.undefined
42 | // Needs to be verified if types align with JS.
43 | )
44 |
45 | override val component = AntCard.Card
46 | }
47 |
48 | @react object CardMeta extends ExternalComponent {
49 | case class Props(avatar: UndefOr[ReactElement] = js.undefined,
50 | className: UndefOr[String] = js.undefined,
51 | description: UndefOr[ReactElement] = js.undefined,
52 | style: UndefOr[js.Object] = js.undefined,
53 | title: UndefOr[ReactElement] = js.undefined)
54 |
55 | override val component = AntCard.Card.Meta
56 | }
57 |
58 | @react object CardGrid extends ExternalComponent {
59 | case class Props(className: UndefOr[String] = js.undefined,
60 | style: UndefOr[js.Object] = js.undefined)
61 |
62 | override val component = AntCard.Card.Grid
63 | }
64 |
--------------------------------------------------------------------------------
/web_client/src/main/scala/com/mypackage/Bootstrap.scala:
--------------------------------------------------------------------------------
1 | package com.mypackage
2 |
3 | import com.apollographql.scalajs.ApolloBoostClient
4 | import com.apollographql.scalajs.ApolloBoostClientOptions
5 | import com.apollographql.scalajs.ApolloError
6 | import com.apollographql.scalajs.link.Operation
7 | import com.apollographql.scalajs.react.ApolloProvider
8 | import com.mypackage.util.UrlUtils
9 | import org.scalajs.dom
10 | import org.scalajs.dom.ext.LocalStorage
11 | import slinky.hot
12 | import slinky.web.ReactDOM
13 |
14 | import scala.scalajs.LinkingInfo
15 | import scala.scalajs.js
16 | import scala.scalajs.js.Dictionary
17 | import scala.scalajs.js.annotation.JSExportTopLevel
18 | import scala.scalajs.js.annotation.JSImport
19 | import scala.scalajs.js.JSConverters._
20 |
21 | @JSImport("antd/dist/antd.css", JSImport.Default)
22 | @js.native
23 | object AntdCSS extends js.Object
24 |
25 | object Bootstrap {
26 |
27 | val css = AntdCSS
28 |
29 | @JSExportTopLevel("entrypoint.main")
30 | def main(): Unit = {
31 | if (LinkingInfo.developmentMode) {
32 | hot.initialize()
33 | }
34 |
35 | val container = Option(dom.document.getElementById("root")).getOrElse {
36 | val elem = dom.document.createElement("div")
37 | elem.id = "root"
38 | dom.document.body.appendChild(elem)
39 | elem
40 | }
41 |
42 | val errorHandler: js.Function1[ApolloError, js.Any] = error => {
43 | if (error.networkError != null) {
44 | if (error.networkError.statusCode == 401) {
45 | LocalStorage.remove("bearerToken")
46 | UrlUtils.navigate("/login")
47 | } else {
48 | println(s"Status: ${error.networkError.statusCode} - Body: ${error.networkError.bodyText}")
49 | }
50 | }
51 | }
52 |
53 | val injectTokenInRequest: js.Function1[Operation, js.Promise[js.Any]] = operation => {
54 | js.Promise.resolve[js.Any](
55 | LocalStorage("bearerToken").map { token =>
56 | operation.setContext(
57 | Map(
58 | "headers" -> Map("X-Auth-Token" -> token).toJSDictionary
59 | ).toJSDictionary.asInstanceOf[Dictionary[js.Any]]
60 | )
61 | js.Object()
62 | }.getOrElse(js.Object()).asInstanceOf[js.Any]
63 | )
64 | }
65 |
66 | val client = new ApolloBoostClient(
67 | ApolloBoostClientOptions(
68 | uri = "/graphql",
69 | onError = errorHandler,
70 | request = injectTokenInRequest
71 | )
72 | )
73 |
74 | ReactDOM.render(ApolloProvider(client)(App()), container)
75 |
76 | }
77 | }
78 |
--------------------------------------------------------------------------------
/web_client/src/main/scala/antd/Layout.scala:
--------------------------------------------------------------------------------
1 | package antd
2 |
3 | import slinky.core.ExternalComponent
4 | import slinky.core.annotations.react
5 | import slinky.core.facade.ReactElement
6 |
7 | import scala.scalajs.js
8 | import scala.scalajs.js.UndefOr
9 | import scala.scalajs.js.annotation.JSImport
10 | import scala.scalajs.js.|
11 |
12 | @JSImport("antd", JSImport.Default)
13 | @js.native
14 | object AntLayout extends js.Object {
15 | val Layout: AntLayoutObj = js.native
16 | }
17 |
18 | @js.native
19 | trait AntLayoutObj extends js.Object {
20 | val Header: js.Object = js.native
21 | val Footer: js.Object = js.native
22 | val Content: js.Object = js.native
23 | val Sider: js.Object = js.native
24 | }
25 |
26 | @react object Layout extends ExternalComponent {
27 | case class Props(className: UndefOr[String] = js.undefined,
28 | hasSider: UndefOr[Boolean] = js.undefined,
29 | style: UndefOr[js.Object] = js.undefined)
30 |
31 | override val component = AntLayout.Layout
32 | }
33 |
34 | @react object LayoutHeader extends ExternalComponent {
35 | case class Props(className: UndefOr[String] = js.undefined,
36 | hasSider: UndefOr[Boolean] = js.undefined,
37 | style: UndefOr[js.Object] = js.undefined)
38 |
39 | override val component = AntLayout.Layout.Header
40 | }
41 |
42 | @react object LayoutFooter extends ExternalComponent {
43 | case class Props(className: UndefOr[String] = js.undefined,
44 | hasSider: UndefOr[Boolean] = js.undefined,
45 | style: UndefOr[js.Object] = js.undefined)
46 |
47 | override val component = AntLayout.Layout.Footer
48 | }
49 |
50 | @react object LayoutContent extends ExternalComponent {
51 | case class Props(className: UndefOr[String] = js.undefined,
52 | hasSider: UndefOr[Boolean] = js.undefined,
53 | style: UndefOr[js.Object] = js.undefined)
54 |
55 | override val component = AntLayout.Layout.Content
56 | }
57 |
58 | @react object LayoutSider extends ExternalComponent {
59 | case class Props(breakpoint: UndefOr[String] = js.undefined,
60 | className: UndefOr[String] = js.undefined,
61 | collapsed: UndefOr[Boolean] = js.undefined,
62 | collapsedWidth: Int = 80,
63 | collapsible: Boolean = false,
64 | defaultCollapsed: Boolean = false,
65 | reverseArrow: Boolean = false,
66 | style: UndefOr[js.Object] = js.undefined,
67 | theme: String = "dark",
68 | trigger: UndefOr[String | ReactElement] = js.undefined,
69 | width: Int | String = 200,
70 | onCollapse: UndefOr[js.Function2[js.Object, js.Object, js.Any]] = js.undefined,
71 | onBreakpoint: UndefOr[js.Function1[js.Object, js.Any]] = js.undefined)
72 |
73 | override val component = AntLayout.Layout.Sider
74 | }
--------------------------------------------------------------------------------
/web_client/src/main/scala/com/mypackage/product/ProductForm.scala:
--------------------------------------------------------------------------------
1 | package com.mypackage.product
2 |
3 | import antd.Button
4 | import antd.FieldDecoratorOptions
5 | import antd.Form
6 | import antd.FormItem
7 | import antd.FormOps
8 | import antd.Input
9 | import antd.StaticProps
10 | import antd.ValidationRules
11 | import com.apollographql.scalajs.react.Mutation
12 | import com.apollographql.scalajs.react.Mutation.StaticMutationAsyncCallback
13 | import com.apollographql.scalajs.react.UpdateStrategy
14 | import com.mypackage.AddProductMutation
15 | import org.scalajs.dom.Event
16 | import slinky.core.StatelessComponent
17 | import slinky.core.annotations.react
18 | import slinky.core.facade.ReactElement
19 | import slinky.web.html.autoComplete
20 | import slinky.web.html.placeholder
21 |
22 | import scala.collection.mutable
23 | import scala.concurrent.ExecutionContext.Implicits.global
24 | import scala.scalajs.js.JSConverters._
25 | import scala.scalajs.js.JSON
26 | import scala.scalajs.js.RegExp
27 |
28 | @react class ProductForm extends StatelessComponent {
29 | type Props = Unit
30 |
31 | def handleSubmit(e: Event,
32 | submitCall: StaticMutationAsyncCallback[AddProductMutation.type]): Unit = {
33 | e.preventDefault()
34 | form.validateFields((errors, values) => {
35 | if (errors != null) {
36 | println("Received errors " + JSON.stringify(errors))
37 | } else {
38 | println("Received values " + JSON.stringify(values))
39 | val valuesMap: mutable.Map[String, String] = values
40 | submitCall(AddProductMutation.Variables(valuesMap("productName"), valuesMap("productDescription"))).onComplete { _ =>
41 | form.setFieldsValue(Map("productName" -> "", "productDescription" -> "").toJSDictionary)
42 | }
43 | }
44 | })
45 | }
46 |
47 | override def render(): ReactElement =
48 | Mutation(AddProductMutation, UpdateStrategy(refetchQueries = Seq("AllProducts"))) { (addProduct, mutationStatus) =>
49 | Form(Form.Props(onSubmit = (e: Event) => {
50 | handleSubmit(e, addProduct)
51 | }))(
52 | FormItem(
53 | form.getFieldDecorator("productName", FieldDecoratorOptions(rules = Seq(ValidationRules(required = true, message = "Cannot contain numbers or be empty.", pattern = RegExp("^[^0-9]+$")))))(
54 | Input(Input.Props())(placeholder := "Name", autoComplete := "off")
55 | )
56 | ),
57 | FormItem(
58 | form.getFieldDecorator("productDescription", FieldDecoratorOptions(rules = Seq(ValidationRules(required = true, message = "Cannot be empty."))))(
59 | Input(Input.Props())(placeholder := "Description", autoComplete := "off")
60 | )
61 | ),
62 | Button(Button.Props(`type` = "primary", htmlType = "submit"))("Add")
63 | )
64 | }
65 |
66 | /**
67 | * This is needed as Antd forms are created as higher order component
68 | * and a form property is injected in the props by the framework.
69 | * This is a workaround to access that property.
70 | */
71 | def form: FormOps = this.asInstanceOf[StaticProps].props.form
72 | }
73 |
--------------------------------------------------------------------------------
/project/QuickCompiler.scala:
--------------------------------------------------------------------------------
1 | import java.util
2 |
3 | import sbt.File
4 | import sbt.internal.inc.CompilerCache
5 | import sbt.internal.util.ManagedLogger
6 | import xsbti.AnalysisCallback
7 | import xsbti.Position
8 | import xsbti.Severity
9 | import xsbti.UseScope
10 | import xsbti.api.ClassLike
11 | import xsbti.api.DependencyContext
12 | import xsbti.compile.Compilers
13 | import xsbti.compile.DependencyChanges
14 |
15 | /**
16 | * Code taken from https://github.com/etsy/sbt-compile-quick-plugin
17 | */
18 | object QuickCompiler {
19 |
20 | def scalac(compilers: Compilers,
21 | sources: Seq[File],
22 | changes: DependencyChanges,
23 | classpath: Seq[File],
24 | outputDir: File,
25 | options: Seq[String],
26 | callback: AnalysisCallback,
27 | maxErrors: Int,
28 | log: ManagedLogger): Unit = {
29 | compilers.scalac() match {
30 | case c: sbt.internal.inc.AnalyzingCompiler =>
31 | c.apply(
32 | sources.toArray,
33 | changes,
34 | classpath.toArray,
35 | outputDir,
36 | options.toArray,
37 | callback,
38 | maxErrors,
39 | new CompilerCache(5000),
40 | log
41 | )
42 | case unknown_compiler =>
43 | log.error("wrong compiler, expected 'sbt.internal.inc.AnalyzingCompiler' got: " + unknown_compiler.getClass.getName)
44 | }
45 | }
46 |
47 | // Indicates to the compiler that no files or dependencies have changed
48 | // This prevents compiling anything other than the requested file
49 | val noChanges: DependencyChanges = new xsbti.compile.DependencyChanges {
50 | def isEmpty = true
51 |
52 | def modifiedBinaries = Array()
53 |
54 | def modifiedClasses = Array()
55 | }
56 |
57 | // This discards the analysis produced by compiling one file, as it
58 | // isn't that useful
59 | object noopCallback extends xsbti.AnalysisCallback {
60 | override def startSource(source: File): Unit = {}
61 |
62 | override def mainClass(sourceFile: File, className: String): Unit = {}
63 |
64 | override def apiPhaseCompleted(): Unit = {}
65 |
66 | override def enabled(): Boolean = false
67 |
68 | override def binaryDependency(onBinaryEntry: File, onBinaryClassName: String, fromClassName: String, fromSourceFile: File, context: DependencyContext): Unit = {}
69 |
70 | override def generatedNonLocalClass(source: File, classFile: File, binaryClassName: String, srcClassName: String): Unit = {}
71 |
72 | override def problem(what: String, pos: Position, msg: String, severity: Severity, reported: Boolean): Unit = {}
73 |
74 | override def dependencyPhaseCompleted(): Unit = {}
75 |
76 | override def classDependency(onClassName: String, sourceClassName: String, context: DependencyContext): Unit = {}
77 |
78 | override def generatedLocalClass(source: File, classFile: File): Unit = {}
79 |
80 | override def api(sourceFile: File, classApi: ClassLike): Unit = {}
81 |
82 | override def usedName(className: String, name: String, useScopes: util.EnumSet[UseScope]): Unit = {}
83 | }
84 | }
85 |
--------------------------------------------------------------------------------
/web_client/src/main/scala/antd/Menu.scala:
--------------------------------------------------------------------------------
1 | package antd
2 |
3 | import slinky.core.ExternalComponent
4 | import slinky.core.annotations.react
5 | import slinky.core.facade.ReactElement
6 |
7 | import scala.scalajs.js
8 | import scala.scalajs.js.UndefOr
9 | import scala.scalajs.js.annotation.JSImport
10 | import scala.scalajs.js.|
11 |
12 | @JSImport("antd", JSImport.Default)
13 | @js.native
14 | object AntMenu extends js.Object {
15 | val Menu: AntMenuObject = js.native
16 | }
17 |
18 | @js.native
19 | trait AntMenuObject extends js.Object {
20 | val Item: js.Object = js.native
21 | val SubMenu: js.Object = js.native
22 | val ItemGroup: js.Object = js.native
23 | val Divider: js.Object = js.native
24 | }
25 |
26 | @react object Menu extends ExternalComponent {
27 | case class Props(defaultOpenKeys: UndefOr[js.Array[String]] = js.undefined,
28 | defaultSelectedKeys: UndefOr[js.Array[String]] = js.undefined,
29 | forceSubMenuRender: Boolean = false,
30 | inlineCollapsed: UndefOr[Boolean] = js.undefined,
31 | inlineIndent: Int = 24,
32 | mode: String = "vertical",
33 | multiple: Boolean = false,
34 | openKeys: UndefOr[js.Array[String]] = js.undefined,
35 | selectable: Boolean = true,
36 | selectedKeys: UndefOr[js.Array[String]] = js.undefined,
37 | style: UndefOr[js.Object] = js.undefined,
38 | subMenuCloseDelay: Double = 0.1,
39 | subMenuOpenDelay: Double = 0.0,
40 | theme: String = "light",
41 | onClick: UndefOr[js.Function3[js.Object, js.Object, js.Object, js.Any]] = js.undefined,
42 | onDeselect: UndefOr[js.Function3[js.Object, js.Object, js.Object, js.Any]] = js.undefined,
43 | onOpenChange: UndefOr[js.Function1[js.Array[String], js.Any]] = js.undefined,
44 | onSelect: UndefOr[js.Function3[js.Object, js.Object, js.Object, js.Any]] = js.undefined)
45 |
46 | override val component = AntMenu.Menu
47 | }
48 |
49 | @react object MenuItem extends ExternalComponent {
50 | case class Props(disabled: Boolean = false,
51 | key: UndefOr[String] = js.undefined,
52 | title: UndefOr[String] = js.undefined)
53 |
54 | override val component = AntMenu.Menu.Item
55 | }
56 |
57 | @react object SubMenu extends ExternalComponent {
58 | case class Props(disabled: Boolean = false,
59 | key: UndefOr[String] = js.undefined,
60 | title: UndefOr[String] = js.undefined,
61 | onTitleClick: UndefOr[js.Function2[js.Object, js.Object, js.Any]] = js.undefined)
62 |
63 | override val component = AntMenu.Menu.SubMenu
64 | }
65 |
66 | @react object ItemGroup extends ExternalComponent {
67 | case class Props(title: UndefOr[String | ReactElement] = js.undefined)
68 |
69 | override val component = AntMenu.Menu.ItemGroup
70 | }
71 |
72 | @react object Divider extends ExternalComponent {
73 | type Props = Unit
74 |
75 | override val component = AntMenu.Menu.Divider
76 | }
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # README #
2 |
3 | Seed project for Play framework + ScalaJS using React, Apollo client and Sangria.
4 | Under the hood it uses:
5 | - GraphQL
6 | - Apollo Client
7 | - Play framework
8 | - Sangria
9 | - Silhouette (Authentication)
10 | - Slinky (React for Scala.js)
11 | - Antd (Components for React)
12 | - Slick with codegen and play-evolutions
13 |
14 |
15 | ## Development Guide
16 |
17 | ### Prerequisites
18 |
19 | 1. Docker installed on the dev box.
20 | See: [https://github.com/frgomes/bash-scripts/blob/master/sysadmin-install/install-docker-ce.sh](https://github.com/frgomes/bash-scripts/blob/master/sysadmin-install/install-docker-ce.sh)
21 |
22 | 2. Node installed on the dev box.
23 | See: [http://github.com/frgomes/bash-scripts/blob/master/user-install/install-node.sh](http://github.com/frgomes/bash-scripts/blob/master/user-install/install-node.sh)
24 |
25 | 3. SBT installed on the dev box
26 | See: [https://github.com/frgomes/bash-scripts/blob/master/user-install/install-scala.sh](https://github.com/frgomes/bash-scripts/blob/master/user-install/install-scala.sh)
27 |
28 | 4. Follow the instructions in the file `project/WebClient.scala`.
29 |
30 | 5. If an error at build time appears saying that module `style-loader` cannot be found, run `npm install -g style-loader css-loader`
31 |
32 | ### IDE support
33 | Follow the setup [here](https://slinky.shadaj.me/docs/installation/) for IntelliJ support.
34 |
35 | ### Build instructions
36 |
37 | * Make sure you have already built and published locally ``apollo-scalajs`` according to instructions at `project/WebClient.scala`.
38 |
39 | * Start a MySQL container via Docker Compose.
40 | ```bash
41 | $ docker-compose up -d
42 | ```
43 |
44 | **Note**: Only MySQL is supported at the moment. In order to support other databases, we should have multiple sets of DDLs and evolutions per database vendor.
45 |
46 | * Run test cases:
47 | ```bash
48 | $ sbt clean test
49 | ```
50 |
51 | * Runs the server and live reloads changes:
52 | ```
53 | $ sbt run
54 | ```
55 |
56 | * Generates a fat jar:
57 | ```
58 | $ sbt assembly
59 | ```
60 |
61 | ## Code generation
62 |
63 | Generates schema.graphql and query/mutation objects for the client.
64 | You shouldn't need to use this explicitly.
65 | ```
66 | $ sbt web_client/Compile/managedSources
67 | ```
68 |
69 | ## Usage
70 |
71 | The default login for the user interface is:
72 | - username: admin
73 | - password: admin
74 |
75 | It is created via evolutions, remove it from there if you want to change it. Also if you want to create another one manually there is an app called _ManualUserGenerator_ to do that.
76 |
77 | ## Problems
78 |
79 | - WebpackDevServer is not used at the moment
80 | - When a new dependency is added to the npmDependencies the bundle.js is not rebuilt.
81 | - The build is slow because some files are not cached (static query generation and two rounds of js bundling) and the dist directory is deleted.
82 | - Add test framework for front-end
83 | - Refetching the queries instead of changing the apollo cache.
84 |
85 | ## Credits
86 |
87 | https://github.com/shadaj/create-react-scala-app.g8/tree/master/src/main/g8
88 |
89 | https://github.com/boosh/play-scalajs-seed/blob/master/build.sbt
90 |
91 | https://github.com/KyleU/boilerplay
92 |
--------------------------------------------------------------------------------
/project/Server.scala:
--------------------------------------------------------------------------------
1 | import com.typesafe.config.ConfigFactory
2 | import com.typesafe.sbt.jse.JsEngineImport.JsEngineKeys
3 | import com.typesafe.sbt.packager.archetypes.JavaAppPackaging
4 | import com.typesafe.sbt.packager.docker.DockerPlugin
5 | import com.typesafe.sbt.packager.jdkpackager.JDKPackagerPlugin
6 | import com.typesafe.sbt.packager.universal.UniversalPlugin
7 | import com.typesafe.sbt.web.Import._
8 | import com.typesafe.sbt.web.SbtWeb
9 | import play.routes.compiler.InjectedRoutesGenerator
10 | import play.sbt.PlayImport
11 | import play.sbt.PlayImport.PlayKeys
12 | import play.sbt.routes.RoutesKeys
13 | import sbt.Keys._
14 | import sbt._
15 | import sbtassembly.AssemblyPlugin.autoImport._
16 | import scalajsbundler.sbtplugin.WebScalaJSBundlerPlugin
17 | import webscalajs.WebScalaJS.autoImport._
18 |
19 | object Server {
20 |
21 | val playSlickV = "4.0.0"
22 | val slickV = "3.3.0"
23 | val silhouetteV = "6.0.0-SNAPSHOT"
24 |
25 | private[this] val dependencies = {
26 | Seq(
27 | PlayImport.guice,
28 | PlayImport.evolutions,
29 | "net.codingwell" %% "scala-guice" % "4.2.3",
30 | "org.sangria-graphql" %% "sangria-play-json" % "1.0.5",
31 | "com.typesafe.play" %% "play-slick" % playSlickV,
32 | "com.typesafe.play" %% "play-slick-evolutions" % playSlickV,
33 | "com.typesafe.slick" %% "slick" % slickV,
34 | "com.typesafe.slick" %% "slick-codegen" % slickV,
35 | "mysql" % "mysql-connector-java" % "6.0.6",
36 | "org.postgresql" % "postgresql" % "42.2.5",
37 | "com.mohiva" %% "play-silhouette" % silhouetteV,
38 | "com.mohiva" %% "play-silhouette-password-bcrypt" % silhouetteV,
39 | "com.mohiva" %% "play-silhouette-crypto-jca" % silhouetteV,
40 | "com.mohiva" %% "play-silhouette-persistence" % silhouetteV,
41 | "com.github.tototoshi" %% "slick-joda-mapper" % "2.4.0"
42 | )
43 | }
44 |
45 |
46 | private[this] lazy val serverSettings = Shared.commonSettings ++ DatabaseUtils.settings ++ Seq(
47 | name := Shared.projectId,
48 | description := Shared.projectName,
49 |
50 | resolvers ++= Seq(
51 | Resolver.jcenterRepo,
52 | Resolver.bintrayRepo("stanch", "maven"),
53 | "Sonatype OSS Snapshots" at "https://oss.sonatype.org/content/repositories/snapshots/"
54 | ),
55 |
56 | libraryDependencies ++= dependencies,
57 |
58 | // Play
59 | RoutesKeys.routesGenerator := InjectedRoutesGenerator,
60 | PlayKeys.externalizeResources := false,
61 | PlayKeys.devSettings := Seq("play.server.akka.requestTimeout" -> "infinite"),
62 | PlayKeys.playDefaultPort := Shared.projectPort,
63 | PlayKeys.playInteractionMode := PlayUtils.NonBlockingInteractionMode,
64 |
65 | // Scala.js
66 | scalaJSProjects := Seq(WebClient.web_client),
67 |
68 | // Sbt-Web
69 | JsEngineKeys.engineType := JsEngineKeys.EngineType.Node,
70 | pipelineStages in Assets := Seq(scalaJSPipeline),
71 |
72 | // Fat-Jar Assembly
73 | assemblyJarName in assembly := Shared.projectId + ".jar",
74 | assemblyMergeStrategy in assembly := {
75 | case "play/reference-overrides.conf" => MergeStrategy.concat
76 | case x => (assemblyMergeStrategy in assembly).value(x)
77 | },
78 | mainClass in assembly := Some("Bootstrap"),
79 | fullClasspath in assembly += Attributed.blank(PlayKeys.playPackageAssets.value),
80 | )
81 |
82 | lazy val server = (project in file("server"))
83 | .enablePlugins(
84 | SbtWeb, play.sbt.PlayScala, JavaAppPackaging,
85 | UniversalPlugin, DockerPlugin, JDKPackagerPlugin, WebScalaJSBundlerPlugin
86 | )
87 | .settings(serverSettings: _*)
88 | .settings(Packaging.settings: _*)
89 | .dependsOn(Shared.sharedJvm, WebClient.web_client, Schema.schema)
90 | }
91 |
--------------------------------------------------------------------------------
/project/WebClient.scala:
--------------------------------------------------------------------------------
1 | import org.portablescala.sbtplatformdeps.PlatformDepsPlugin.autoImport._
2 | import org.scalajs.sbtplugin.ScalaJSPlugin
3 | import org.scalajs.sbtplugin.ScalaJSPlugin.autoImport._
4 | import sbt.Keys._
5 | import sbt._
6 | import scalajsbundler.sbtplugin.ScalaJSBundlerPlugin
7 | import scalajsbundler.sbtplugin.ScalaJSBundlerPlugin.autoImport._
8 | import webscalajs.ScalaJSWeb
9 |
10 | object WebClient {
11 |
12 | val slinkyVer = "0.6.0"
13 | val reactVer = "16.5.2"
14 |
15 | /*
16 | * Some dependencies needed to make this project work are in the master branch, still to be released.
17 | *
18 | * In order to make this project work, follow these steps:
19 | *
20 | * 1. Clone the repo https://github.com/frgomes/apollo-scalajs
21 | * ```
22 | * $ git clone https://github.com/frgomes/apollo-scalajs
23 | * ```
24 | * 2. Run SBT, publishLocal and obtain the published version information:
25 | * ```
26 | * $ cd apollo-scalajs
27 | * $ sbt
28 | * > publishLocal
29 | * > show core/version
30 | * 0.7.0+26-5cd49197+20190616-1445
31 | * > quit
32 | * ```
33 | * 3. update variable ``apolloScalaJsVer`` so that it contains the version information just obtained.
34 | */
35 | val apolloScalaJsVer = "0.7.0+27-0badbdb2"
36 |
37 |
38 | private[this] val clientSettings =
39 | Shared.commonSettings ++ WebpackUtils.wiringTasks ++ GraphqlUtils.generateTasks ++ Seq(
40 | scalaJSUseMainModuleInitializer := true, // Starts scalajs from a main function
41 | mainClass in Compile := Some("com.mypackage.Bootstrap"),
42 |
43 | resolvers ++= Seq(
44 | "Apollo Bintray" at "https://dl.bintray.com/apollographql/maven/",
45 | Resolver.bintrayRepo("hmil", "maven")
46 | ),
47 |
48 | libraryDependencies ++= Seq(
49 | "me.shadaj" %%% "slinky-web" % slinkyVer, // React DOM, HTML and SVG tags
50 | "me.shadaj" %%% "slinky-hot" % slinkyVer, // Hot loading, requires react-proxy package
51 | "me.shadaj" %%% "slinky-scalajsreact-interop" % slinkyVer, // Interop with japgolly/scalajs-react
52 | "com.apollographql" %%% "apollo-scalajs-core" % apolloScalaJsVer,
53 | "com.apollographql" %%% "apollo-scalajs-react" % apolloScalaJsVer,
54 | "org.sangria-graphql" %% "sangria-circe" % "1.1.0",
55 | "fr.hmil" %%% "roshttp" % "2.2.4"
56 | ),
57 | Compile / npmDependencies ++= Seq(
58 | "react" -> reactVer,
59 | "react-dom" -> reactVer,
60 | "react-proxy" -> "1.1.8",
61 | "react-router-dom" -> "5.0.0",
62 | "apollo-boost" -> "0.1.16",
63 | "apollo-cache-inmemory" -> "1.4.3",
64 | "react-apollo" -> "2.2.2",
65 | "graphql-tag" -> "2.10.0",
66 | "graphql" -> "14.0.2",
67 | "antd" -> "3.13.0"
68 | ),
69 |
70 | Compile / npmDevDependencies ++= Seq(
71 | "file-loader" -> "2.0.0",
72 | "style-loader" -> "0.23.1",
73 | "css-loader" -> "1.0.0",
74 | "copy-webpack-plugin" -> "4.5.4",
75 | "clean-webpack-plugin" -> "1.0.0",
76 | "webpack-merge" -> "4.1.4",
77 | "apollo" -> "2.5.3"
78 | ),
79 |
80 | webpack / version := "4.21.0",
81 | startWebpackDevServer / version := "3.1.14",
82 |
83 | webpackResources := baseDirectory.value / "webpack" * "*",
84 | fastOptJS / webpackConfigFile := Some(baseDirectory.value / "webpack" / "webpack-fastopt.config.js"),
85 | fullOptJS / webpackConfigFile := Some(baseDirectory.value / "webpack" / "webpack-opt.config.js"),
86 |
87 | fastOptJS / webpackBundlingMode := BundlingMode.LibraryOnly(),
88 |
89 | scalacOptions ++= Seq("-P:scalajs:sjsDefinedByDefault", "-P:scalajs:suppressExportDeprecations"),
90 |
91 | GraphqlUtils.npmUpdateTask := (Compile / npmInstallDependencies).value,
92 |
93 | )
94 |
95 | lazy val web_client = (project in file("web_client"))
96 | .settings(clientSettings: _*)
97 | // .settings(addCommandAlias("dev", ";fastOptJS::startWebpackDevServer;~fastOptJS"): _*)
98 | .enablePlugins(ScalaJSPlugin, ScalaJSWeb, ScalaJSBundlerPlugin)
99 | .dependsOn(Shared.sharedJs, Schema.schema)
100 | }
101 |
102 |
103 |
--------------------------------------------------------------------------------
/project/Shared.scala:
--------------------------------------------------------------------------------
1 | import sbt.Keys._
2 | import sbt._
3 | import sbtassembly.AssemblyPlugin.autoImport._
4 | import sbtcrossproject.CrossPlugin.autoImport.{crossProject, CrossType, _}
5 | import scalajscrossproject.ScalaJSCrossPlugin.autoImport._
6 | import webscalajs.ScalaJSWeb
7 |
8 | object Shared {
9 | val projectId = "scala-graphql-fullstack-seed"
10 | val projectName = "scala-graphql-fullstack-seed"
11 | val projectPort = 9000
12 |
13 | object Versions {
14 | val app = "1.0.0"
15 | val scala = "2.12.8"
16 | }
17 |
18 | private[this] val profilingEnabled = false
19 |
20 | val compileOptions = Seq(
21 | "-target:jvm-1.8", "-encoding", "UTF-8", "-feature", "-deprecation", "-explaintypes", "-unchecked",
22 | "–Xcheck-null", "-Xfatal-warnings", /* "-Xlint", */ "-Xcheckinit", "-Xfuture", "-Yrangepos", "-Ypartial-unification",
23 | "-Yno-adapted-args", "-Ywarn-inaccessible", "-Ywarn-nullary-override", "-Ywarn-numeric-widen", "-Ywarn-infer-any",
24 | "-language:postfixOps"
25 | ) ++ (if (profilingEnabled) {
26 | "-Ystatistics:typer" +: Seq("no-profiledb", "show-profiles", "generate-macro-flamegraph").map(s => s"-P:scalac-profiling:$s")
27 | } else { Nil })
28 |
29 | lazy val commonSettings = Seq(
30 | version := Shared.Versions.app,
31 | scalaVersion := Shared.Versions.scala,
32 |
33 | scalacOptions ++= compileOptions,
34 | scalacOptions in (Compile, console) ~= (_.filterNot(Set("-Ywarn-unused:imports", "-Xfatal-warnings"))),
35 | scalacOptions in (Compile, doc) := Seq("-encoding", "UTF-8"),
36 |
37 | publishMavenStyle := false,
38 |
39 | evictionWarningOptions in update := EvictionWarningOptions.default.withWarnTransitiveEvictions(false).withWarnDirectEvictions(false),
40 |
41 | test in assembly := {},
42 | assemblyMergeStrategy in assembly := {
43 | case PathList("javax", "servlet", _ @ _*) => MergeStrategy.first
44 | case PathList("javax", "xml", _ @ _*) => MergeStrategy.first
45 | case PathList(p @ _*) if p.last.contains("about_jetty-") => MergeStrategy.discard
46 | case PathList("org", "apache", "commons", "logging", _ @ _*) => MergeStrategy.first
47 | case PathList("org", "w3c", "dom", _ @ _*) => MergeStrategy.first
48 | case PathList("org", "w3c", "dom", "events", _ @ _*) => MergeStrategy.first
49 | case PathList("javax", "annotation", _ @ _*) => MergeStrategy.first
50 | case PathList("net", "jcip", "annotations", _ @ _*) => MergeStrategy.first
51 | case PathList("play", "api", "libs", "ws", _ @ _*) => MergeStrategy.first
52 | case PathList("META-INF", "io.netty.versions.properties") => MergeStrategy.first
53 | case PathList("sqlj", _ @ _*) => MergeStrategy.first
54 | case PathList("play", "reference-overrides.conf") => MergeStrategy.first
55 | case PathList("version","util", "Version$.class") => MergeStrategy.first
56 | case "module-info.class" => MergeStrategy.discard
57 | case "messages" => MergeStrategy.concat
58 | case "pom.xml" => MergeStrategy.discard
59 | case "JS_DEPENDENCIES" => MergeStrategy.discard
60 | case "NPM_DEPENDENCIES" => MergeStrategy.discard
61 | case "pom.properties" => MergeStrategy.discard
62 | case "application.conf" => MergeStrategy.concat
63 | case x => (assemblyMergeStrategy in assembly).value(x)
64 | },
65 |
66 | // Macros
67 | addCompilerPlugin("org.scalamacros" % "paradise" % "2.1.0" cross CrossVersion.full),
68 |
69 | ) ++ (if(profilingEnabled) {
70 | Seq(addCompilerPlugin("ch.epfl.scala" %% "scalac-profiling" % "1.0.0"))
71 | } else {
72 | Nil
73 | })
74 |
75 | lazy val sharedSettings = commonSettings ++ Seq(
76 | Compile / sourceGenerators += ProjectVersionManager.writeConfig(projectId, projectName, projectPort).taskValue,
77 | libraryDependencies ++= Seq()
78 | )
79 |
80 | lazy val shared = (crossProject(JSPlatform, JVMPlatform)
81 | .withoutSuffixFor(JVMPlatform)
82 | .crossType(CrossType.Pure) in file("shared"))
83 | .settings(sharedSettings: _*)
84 | .jvmSettings(libraryDependencies += "org.scala-js" %% "scalajs-stubs" % "0.6.26" % "provided")
85 |
86 | lazy val sharedJs = shared.js.enablePlugins(ScalaJSWeb)
87 |
88 | lazy val sharedJvm = shared.jvm
89 | }
90 |
--------------------------------------------------------------------------------
/web_client/src/main/scala/com/mypackage/authentication/Login.scala:
--------------------------------------------------------------------------------
1 | package com.mypackage.authentication
2 |
3 | import antd.AntMessage
4 | import antd.Button
5 | import antd.Col
6 | import antd.FieldDecoratorOptions
7 | import antd.Form
8 | import antd.FormItem
9 | import antd.FormOps
10 | import antd.Input
11 | import antd.Layout
12 | import antd.LayoutContent
13 | import antd.Row
14 | import antd.StaticProps
15 | import antd.ValidationRules
16 | import com.mypackage.AddProductMutation
17 | import com.mypackage.util.UrlUtils
18 | import fr.hmil.roshttp.HttpRequest
19 | import fr.hmil.roshttp.Method
20 | import fr.hmil.roshttp.body.Implicits._
21 | import fr.hmil.roshttp.body.JSONBody._
22 | import org.scalajs.dom.Event
23 | import slinky.core.StatelessComponent
24 | import slinky.core.annotations.react
25 | import slinky.web.html.autoComplete
26 | import slinky.web.html.h1
27 | import slinky.web.html.placeholder
28 | import slinky.web.html.style
29 |
30 | import scala.scalajs.js.JSConverters._
31 | import monix.execution.Scheduler.Implicits.global
32 | import org.scalajs.dom.ext.LocalStorage
33 | import reactrouter.History
34 | import reactrouter.Redirect
35 |
36 | import scala.collection.mutable
37 | import scala.scalajs.js
38 | import scala.scalajs.js.JSON
39 | import scala.util.Failure
40 | import scala.util.Success
41 |
42 | @react class Login extends StatelessComponent {
43 | case class Props(history: History)
44 |
45 | val handleSubmit = (e: Event) => {
46 | e.preventDefault()
47 |
48 | form.validateFields((errors, values) => {
49 | if (errors != null) {
50 | println("Received errors " + JSON.stringify(errors))
51 | } else {
52 | println("Received values " + JSON.stringify(values))
53 | val valuesMap: mutable.Map[String, String] = values
54 |
55 | HttpRequest().withURL(UrlUtils.getBaseUrl + "/authenticate").post(JSONObject(
56 | "username" -> valuesMap("username"),
57 | "password" -> valuesMap("password")
58 | )).onComplete { result =>
59 | form.setFieldsValue(Map("username" -> "", "password" -> "").toJSDictionary)
60 |
61 | result match {
62 | case Failure(exception) =>
63 | AntMessage.message.error("Unable to login, retry!")
64 | println(exception.getMessage)
65 | case Success(result) =>
66 | AntMessage.message.info("Login successful")
67 | LocalStorage.update("bearerToken", result.body)
68 | props.history.push("/")
69 | }
70 | }
71 | }
72 | })
73 | }
74 |
75 | def render() = {
76 | if (LocalStorage("bearerToken").isEmpty) {
77 | Layout(
78 | LayoutContent(
79 | Row(Row.Props(`type` = "flex", align = "middle", justify = "center"))(style := js.Dynamic.literal(height = "100vh"))(
80 | Col(Col.Props(span = 6))(
81 | Form(Form.Props(onSubmit = handleSubmit))(
82 | FormItem(
83 | form.getFieldDecorator("username", FieldDecoratorOptions(rules = Seq(ValidationRules(required = true, message = "Cannot login without username."))))(
84 | Input(Input.Props())(placeholder := "Username")
85 | )
86 | ),
87 | FormItem(
88 | form.getFieldDecorator("password", FieldDecoratorOptions(rules = Seq(ValidationRules(required = true, message = "Cannot login without password."))))(
89 | Input(Input.Props(`type` = "password"))(placeholder := "Password")
90 | )
91 | ),
92 | Button(Button.Props(`type` = "primary", htmlType = "submit"))("Login")
93 | )
94 | )
95 | )
96 | )
97 | )
98 | } else {
99 | Redirect(Redirect.Props(to = "/"))
100 | }
101 | }
102 |
103 | /**
104 | * This is needed as Antd forms are created as higher order component
105 | * and a form property is injected in the props by the framework.
106 | * This is a workaround to access that property.
107 | */
108 | def form: FormOps = this.asInstanceOf[StaticProps].props.form
109 | }
110 |
--------------------------------------------------------------------------------
/project/GraphqlUtils.scala:
--------------------------------------------------------------------------------
1 | import sbt.Keys._
2 | import sbt._
3 | import sbt.internal.inc.classpath.ClasspathUtilities
4 |
5 | /**
6 | * Tools to generate at build time the SDL for graphql from the defined schema in Sangria-Scala.
7 | *
8 | * To be replaced with sbt-graphql. After a fast test i've noticed that when the task in sbt-graphql used
9 | * to generate the schema is added to the (Compile / resourceGenerators) task list, when the project is
10 | * built, sbt enters infinite loop.
11 | */
12 | object GraphqlUtils {
13 |
14 | val generateGraphqlSdlFromScala = taskKey[Seq[File]]("Generates the SDL from the defined schema in Sangria")
15 | val schemaSdlFileName = settingKey[String]("The name of the file generated when the graphql SDL is rendered")
16 |
17 | val generateGraphqlQueries = taskKey[Seq[File]]("Generates Scala classes definitions from queries defined in graphql")
18 | val queryClassesPackage = settingKey[String]("The package the generated Scala classes should belong to")
19 | val npmUpdateTask = taskKey[File]("Task that updates npm and returns the installation directory")
20 |
21 | val generateTasks = Seq(
22 | schemaSdlFileName := "schema.graphql",
23 |
24 | generateGraphqlSdlFromScala := {
25 | val classpath = (dependencyClasspath in Compile).value
26 | val schemaObject = (Schema.schema / Schema.schemaObject).value
27 | val outputDir = (Compile / resourceManaged).value
28 | val outputFile = (Compile / schemaSdlFileName).value
29 | val sdlFile = generateSdl(classpath, schemaObject, outputDir, outputFile)
30 | streams.value.log.info("Written graphql SDL in file " + sdlFile.getAbsolutePath)
31 | Seq(sdlFile)
32 | },
33 |
34 | Compile / resourceGenerators += Compile / generateGraphqlSdlFromScala,
35 |
36 | queryClassesPackage := "com.mypackage",
37 |
38 | generateGraphqlQueries := {
39 | import scala.sys.process._
40 |
41 | (Compile / generateGraphqlSdlFromScala).value
42 |
43 | val outputDirectory = (Compile / sourceManaged).value
44 | val outputFile = outputDirectory / "graphql.scala"
45 |
46 | outputDirectory.mkdirs()
47 |
48 | val apolloDirectory = npmUpdateTask.value / "node_modules" / "apollo"
49 | val queriesDirectory = ((Compile / sourceDirectory).value / "graphql").getAbsolutePath
50 |
51 | val apolloCommand = Seq(
52 | (apolloDirectory / "bin" / "run").getAbsolutePath,
53 | "codegen:generate",
54 | "--includes=" + queriesDirectory + "/*.graphql",
55 | "--localSchemaFile=" + ((Compile / resourceManaged).value / (Compile / schemaSdlFileName).value).getAbsolutePath,
56 | "--namespace=" + queryClassesPackage.value,
57 | "--target=scala",
58 | outputFile.getAbsolutePath
59 | )
60 |
61 | apolloCommand.!
62 |
63 | streams.value.log.info("Running: " + apolloCommand)
64 |
65 | Seq(outputFile)
66 | },
67 |
68 | Compile / sourceGenerators += Compile / generateGraphqlQueries,
69 |
70 | watchSources ++= ((sourceDirectory in Compile).value / "graphql" ** "*.graphql").get
71 | )
72 |
73 | private def generateSdl(classpath: Classpath, schemaObjectFQN: String, outputDir: File, outputFile: String): File = {
74 | def scalaObjectInstance(clazz: Class[_]) = {
75 | val schemaObjectConstructor = clazz.getDeclaredConstructors.head
76 | schemaObjectConstructor.setAccessible(true)
77 | schemaObjectConstructor.newInstance()
78 | }
79 |
80 | def schemaDefinition(classLoader: ClassLoader) = {
81 | val schemaObjectRef = classLoader.loadClass(schemaObjectFQN + "$")
82 | val schemaObjectInstance = scalaObjectInstance(schemaObjectRef)
83 | val schemaField = schemaObjectRef.getMethod("schema")
84 | schemaField.invoke(schemaObjectInstance)
85 | }
86 |
87 | def renderSchema(classLoader: ClassLoader, schema: AnyRef): String = {
88 | val schemaRendererObjectRef = classLoader.loadClass("sangria.renderer.SchemaRenderer" + "$")
89 | val schemaClass = classLoader.loadClass("sangria.schema.Schema")
90 | val schemaRendererMethod = schemaRendererObjectRef.getMethod("renderSchema", schemaClass)
91 | val schemaRendererInstance = scalaObjectInstance(schemaRendererObjectRef)
92 | schemaRendererMethod.invoke(schemaRendererInstance, schema).asInstanceOf[String]
93 | }
94 |
95 | val classLoader = ClasspathUtilities.toLoader(classpath.map(_.data).map(_.getAbsoluteFile))
96 | val schema = schemaDefinition(classLoader)
97 | val sdl = renderSchema(classLoader, schema)
98 | val sdlFile = new File(outputDir, outputFile)
99 | IO.write(sdlFile, sdl)
100 |
101 | sdlFile
102 | }
103 | }
104 |
--------------------------------------------------------------------------------
/server/app/di/DiModule.scala:
--------------------------------------------------------------------------------
1 | package di
2 |
3 | import auth.AuthEnvironment
4 | import auth.PasswordInfoRepository
5 | import auth.User
6 | import auth.UserIdentityService
7 | import auth.UserSessionRepository
8 | import com.google.inject.AbstractModule
9 | import com.google.inject.Provides
10 | import com.google.inject.Singleton
11 | import com.mohiva.play.silhouette.api.Environment
12 | import com.mohiva.play.silhouette.api.EventBus
13 | import com.mohiva.play.silhouette.api.Silhouette
14 | import com.mohiva.play.silhouette.api.SilhouetteProvider
15 | import com.mohiva.play.silhouette.api.repositories.AuthInfoRepository
16 | import com.mohiva.play.silhouette.api.repositories.AuthenticatorRepository
17 | import com.mohiva.play.silhouette.api.services.AuthenticatorService
18 | import com.mohiva.play.silhouette.api.services.IdentityService
19 | import com.mohiva.play.silhouette.api.util.Clock
20 | import com.mohiva.play.silhouette.api.util.IDGenerator
21 | import com.mohiva.play.silhouette.api.util.PasswordHasher
22 | import com.mohiva.play.silhouette.api.util.PasswordHasherRegistry
23 | import com.mohiva.play.silhouette.api.util.PasswordInfo
24 | import com.mohiva.play.silhouette.impl.authenticators.BearerTokenAuthenticator
25 | import com.mohiva.play.silhouette.impl.authenticators.BearerTokenAuthenticatorService
26 | import com.mohiva.play.silhouette.impl.authenticators.BearerTokenAuthenticatorSettings
27 | import com.mohiva.play.silhouette.impl.util.SecureRandomIDGenerator
28 | import com.mohiva.play.silhouette.password.BCryptPasswordHasher
29 | import com.mohiva.play.silhouette.persistence.daos.DelegableAuthInfoDAO
30 | import com.mohiva.play.silhouette.persistence.repositories.DelegableAuthInfoRepository
31 | import com.mypackage.PictureRepoLike
32 | import com.mypackage.ProductRepoLike
33 | import com.mypackage.RequestContext
34 | import net.codingwell.scalaguice.ScalaModule
35 | import play.api.Configuration
36 | import repositories.PictureRepo
37 | import repositories.ProductRepo
38 | import slick.jdbc.JdbcBackend.Database
39 |
40 | import scala.concurrent.ExecutionContext.Implicits.global
41 | import scala.concurrent.duration._
42 | import DiModule._
43 |
44 | class DiModule extends AbstractModule with ScalaModule {
45 |
46 | override def configure(): Unit = {
47 | bind[PictureRepoLike].to[PictureRepo]
48 | bind[ProductRepoLike].to[ProductRepo]
49 | bind[Silhouette[AuthEnvironment]].to[SilhouetteProvider[AuthEnvironment]]
50 | bind[IDGenerator].toInstance(new SecureRandomIDGenerator)
51 | bind[Clock].toInstance(Clock())
52 | bind[EventBus].toInstance(EventBus())
53 | bind[AuthenticatorRepository[BearerTokenAuthenticator]].to[UserSessionRepository].in(classOf[Singleton])
54 | bind[IdentityService[User]].to[UserIdentityService].in(classOf[Singleton])
55 | bind[DelegableAuthInfoDAO[PasswordInfo]].to[PasswordInfoRepository]
56 | bind[PasswordHasher].toInstance(hasher)
57 | }
58 |
59 | @Provides
60 | @Singleton
61 | def getDatabase(): Database = {
62 | Database.forConfig(databaseConfigPath)
63 | }
64 |
65 | @Provides
66 | @Singleton
67 | def getRequestContext(productRepository: ProductRepoLike,
68 | pictureRepository: PictureRepoLike): RequestContext = {
69 | new RequestContext {
70 | override def productRepo: ProductRepoLike = productRepository
71 | override def pictureRepo: PictureRepoLike = pictureRepository
72 | }
73 | }
74 |
75 | @Provides
76 | @Singleton
77 | def getEnvironment(is: IdentityService[User],
78 | as: AuthenticatorService[BearerTokenAuthenticator],
79 | eventBus: EventBus): Environment[AuthEnvironment] =
80 | Environment[AuthEnvironment](is, as, Seq(), eventBus)
81 |
82 |
83 | @Provides
84 | @Singleton
85 | def getAuthenticatorService(configuration: Configuration,
86 | authenticatorRepository: AuthenticatorRepository[BearerTokenAuthenticator],
87 | idGenerator: IDGenerator,
88 | clock: Clock): AuthenticatorService[BearerTokenAuthenticator] = {
89 |
90 | new BearerTokenAuthenticatorService(
91 | BearerTokenAuthenticatorSettings(authenticatorExpiry = 30 days),
92 | authenticatorRepository,
93 | idGenerator,
94 | clock)
95 | }
96 |
97 | @Provides
98 | @Singleton
99 | def getAuthInfoRepository(passwordInfoDAO: DelegableAuthInfoDAO[PasswordInfo]): AuthInfoRepository =
100 | new DelegableAuthInfoRepository(passwordInfoDAO)
101 |
102 | @Provides
103 | @Singleton
104 | def getPasswordHasherRegistry(passwordHasher: PasswordHasher) =
105 | PasswordHasherRegistry(passwordHasher)
106 |
107 | }
108 |
109 | object DiModule {
110 | def databaseConfigPath = "slick.dbs.default.db"
111 | val hasher = new BCryptPasswordHasher()
112 | }
--------------------------------------------------------------------------------
/server/app/views/graphiql.scala.html:
--------------------------------------------------------------------------------
1 | @()
2 |
3 | @main("GraphiQL"){
4 |
15 |
16 |
17 |
18 |
19 |
20 |
21 | }{
22 | Loading...
23 |
24 |
117 | }
118 |
--------------------------------------------------------------------------------
/project/DatabaseUtils.scala:
--------------------------------------------------------------------------------
1 | import com.typesafe.config.ConfigFactory
2 | import sbt.Keys._
3 | import sbt._
4 |
5 | /**
6 | * This utils are used to run the evolutions the first time the project is checked out or
7 | * in case the DB is whiped. This is necessary because if the DB is not initialized before
8 | * the slick code generation is started, the compilation task will fail due to the fact that
9 | * slick code generation task does not generate anything.
10 | */
11 | object DatabaseUtils {
12 | val slickCodeGenTask = taskKey[Seq[File]]("Generates the table mapping classes to be used with slick")
13 | val slickOutputDir = settingKey[File]("The directory where the database mappings are outputted")
14 | val runEvolutions = taskKey[Seq[File]]("Runs the evolutions on the database")
15 |
16 | def generateSlickTables(baseDir: File, outputDir: File, classPath: Seq[File], runner: ScalaRun, stream: TaskStreams): Seq[File] = {
17 | val config = ConfigFactory.parseFile(new File(s"${baseDir.getAbsolutePath}/conf/application.conf")).resolve()
18 | val url = config.getString("slick.dbs.default.db.url")
19 | val jdbcDriver = config.getString("slick.dbs.default.db.driver")
20 | val slickProfile = config.getString("slick.dbs.default.profile").replace('$', ' ').trim
21 | val pkg = config.getString("slick.dbs.default.codegen.package")
22 | val user = config.getString("slick.dbs.default.db.user")
23 | val password = config.getString("slick.dbs.default.db.password")
24 | val ignoreInvalidDefaults = "true"
25 | val generatorClass = "infrastructure.JodaAwareSourceCodeGenerator"
26 | val outputMultipleFiles = "true"
27 | val generatorOptions = Array(slickProfile, jdbcDriver, url, outputDir.getPath, pkg, user, password, ignoreInvalidDefaults, generatorClass, outputMultipleFiles)
28 | runner.run("slick.codegen.SourceCodeGenerator", classPath, generatorOptions, stream.log).failed foreach (sys error _.getMessage)
29 | stream.log.info("Written slick mappings in directory " + outputDir)
30 | (outputDir / pkg.replace('.', '/')).listFiles()
31 | }
32 |
33 | /**
34 | * Copies the application.conf and the evolutions directory in the output directory.
35 | * This is used only when the project is checked out fresh, it is needed to run the
36 | * first time the evolutions. During the development this task is taken care by the
37 | * resourceGenerators
38 | */
39 | def copyConfigAndEvolutions(resourcesDir: File, outputDir: File): Unit = {
40 | if (!(outputDir / "application.conf").exists()) {
41 | // Copy config
42 | val srcConf = resourcesDir / "application.conf"
43 | val dstConf = outputDir / "application.conf"
44 | IO.copyFile(srcConf, dstConf, CopyOptions(true, true, true))
45 |
46 | // Copy evolutions
47 | val srcEvo = resourcesDir / "evolutions/default"
48 | val dstEvo = outputDir / "evolutions/default"
49 | IO.copyDirectory(srcEvo, dstEvo, CopyOptions(true, true, true))
50 | }
51 | }
52 |
53 |
54 | val settings = Seq(
55 |
56 | runEvolutions := {
57 | // Compiling necessary class to run evolutions
58 | val srcDir = (Compile / sourceDirectory).value
59 | val compilers = Keys.compilers.value
60 | val classPath = (Compile / dependencyClasspath).value
61 | val filesToCompile = Seq(
62 | srcDir / "infrastructure/ApplyEvolutions.scala",
63 | srcDir / "infrastructure/JodaAwareSourceCodeGenerator.scala"
64 | )
65 | val outputDir = (Compile / classDirectory).value
66 | val resourcesDir = (Compile / resourceDirectory).value
67 | val options = Shared.compileOptions
68 | val maxErrors = 1000
69 | val stream = streams.value
70 |
71 | copyConfigAndEvolutions(resourcesDir, outputDir)
72 |
73 | QuickCompiler.scalac(
74 | compilers,
75 | filesToCompile,
76 | QuickCompiler.noChanges,
77 | classPath.map(_.data),
78 | outputDir,
79 | options,
80 | QuickCompiler.noopCallback,
81 | maxErrors,
82 | stream.log
83 | )
84 |
85 | // Running evolutions
86 | val baseDir = baseDirectory.value
87 | val runner = (Compile / Keys.runner).value
88 |
89 | runner.run("infrastructure.ApplyEvolutions", classPath.files.+:(outputDir), Array(baseDir.getAbsolutePath), stream.log).failed foreach (sys error _.getMessage)
90 | Seq()
91 | },
92 |
93 | slickOutputDir := {
94 | val baseDir = baseDirectory.value
95 | val sourceManagedDir = sourceManaged.value
96 | val config = ConfigFactory.parseFile(new File(s"${baseDir.getAbsolutePath}/conf/application.conf")).resolve()
97 |
98 | sourceManagedDir / config.getString("slick.dbs.default.codegen.outputDir")
99 | },
100 |
101 | slickCodeGenTask := {
102 | runEvolutions.value
103 | val dir = slickOutputDir.value
104 | val baseDir = baseDirectory.value
105 | val classPath = (Compile / dependencyClasspath).value.files :+ (Compile / classDirectory).value
106 | val runner = (Compile / Keys.runner).value
107 | val stream = streams.value
108 | generateSlickTables(baseDir, dir, classPath, runner, stream)
109 | },
110 |
111 | Compile / managedSourceDirectories += slickOutputDir.value,
112 | Compile / sourceGenerators += slickCodeGenTask,
113 |
114 | )
115 | }
116 |
--------------------------------------------------------------------------------
/web_client/src/main/scala/antd/Form.scala:
--------------------------------------------------------------------------------
1 | package antd
2 |
3 | import antd.FormTypes.Value
4 | import org.scalajs.dom.Event
5 | import slinky.core.ExternalComponent
6 | import slinky.core.ReactComponentClass
7 | import slinky.core.annotations.react
8 | import slinky.core.facade.ReactElement
9 |
10 | import scala.scalajs.js
11 | import scala.scalajs.js.JSConverters._
12 | import scala.scalajs.js.RegExp
13 | import scala.scalajs.js.UndefOr
14 | import scala.scalajs.js.annotation.JSImport
15 | import scala.scalajs.js.|
16 |
17 | /**
18 | * Forms follow the implementation at https://github.com/react-component/form
19 | *
20 | * This representation does not contain all the properties available in the
21 | * JS implementation, only the main have been ported.
22 | */
23 |
24 |
25 | @JSImport("antd", JSImport.Default)
26 | @js.native
27 | object AntForm extends js.Object {
28 | val Form: FormObj = js.native
29 | }
30 |
31 | @js.native
32 | trait FormObj extends js.Object {
33 | val Item: js.Object = js.native
34 |
35 | def create(formOptions: FormOptions = FormOptions()): js.Function1[ReactComponentClass[_], js.Object] = js.native
36 | }
37 |
38 | @js.native
39 | trait FieldsNames extends js.Object {
40 | val names: Seq[String]
41 | }
42 |
43 | object FormTypes {
44 | type Value = String
45 | }
46 |
47 | @js.native
48 | trait FormOptions extends js.Object {
49 | val name: UndefOr[String] = js.native
50 | val validateMessages: js.Object = js.native
51 | }
52 |
53 | object FormOptions {
54 | def apply(name: UndefOr[String] = js.undefined,
55 | validateMessages: js.Object = js.Object()) =
56 | js.Dynamic.literal(
57 | name = name,
58 | validateMessages = validateMessages
59 | ).asInstanceOf[FormOptions]
60 | }
61 |
62 | @js.native
63 | trait FieldProperties extends js.Object {
64 | val value: String = js.native
65 | val errors: js.Array[js.Error] = js.native
66 | }
67 |
68 | object FieldProperties {
69 | def apply(value: String, errors: Seq[js.Error]) =
70 | js.Dynamic.literal(value = value, errors = errors.toJSArray).asInstanceOf[FieldProperties]
71 | }
72 |
73 | @js.native
74 | trait ValidationOptions extends js.Object {
75 | val first: Boolean = js.native
76 | val firstFields: js.Array[String] = js.native
77 | val force: Boolean = js.native
78 | val scroll: js.Object = js.native
79 | }
80 |
81 | object ValidationOptions {
82 | def apply(first: Boolean = true,
83 | firstFields: Seq[String] = Seq(),
84 | force: Boolean = false,
85 | scroll: js.Object = js.Object()) =
86 | js.Dynamic.literal(
87 | first = first,
88 | firstFields = firstFields.toJSArray,
89 | force = force,
90 | scroll = scroll
91 | ).asInstanceOf[ValidationOptions]
92 |
93 | def apply() = js.Object().asInstanceOf[ValidationOptions]
94 | }
95 |
96 | @js.native
97 | trait ValidationError extends js.Object {
98 | val message: String = js.native
99 | val field: String = js.native
100 | }
101 |
102 | @js.native
103 | trait ValidationErrorList extends js.Object {
104 | val errors: js.Array[ValidationError]
105 | }
106 |
107 | @js.native
108 | trait ValidationRules extends js.Object {
109 | val enum: UndefOr[String] = js.native
110 | val len: UndefOr[Int] = js.native
111 | val max: UndefOr[Int] = js.native
112 | val message: UndefOr[String | ReactElement] = js.native
113 | val min: UndefOr[Int] = js.native
114 | val pattern: UndefOr[RegExp] = js.native
115 | val required: Boolean = js.native
116 | val transform: UndefOr[Value => js.Any] = js.native
117 | val `type`: String = js.native
118 | //validator -> TODO = js.native
119 | val whitespace: Boolean = js.native
120 | }
121 |
122 | object ValidationRules {
123 |
124 | def apply(enum: UndefOr[String] = js.undefined,
125 | len: UndefOr[Int] = js.undefined,
126 | max: UndefOr[Int] = js.undefined,
127 | message: UndefOr[String | ReactElement] = js.undefined,
128 | min: UndefOr[Int] = js.undefined,
129 | pattern: UndefOr[RegExp] = js.undefined,
130 | required: Boolean = false,
131 | transform: UndefOr[Value => js.Any] = js.undefined,
132 | `type`: String = "string",
133 | whitespace: Boolean = false) =
134 | js.Dynamic.literal(
135 | "enum" -> enum,
136 | "len" -> len,
137 | "max" -> max,
138 | "message" -> message.asInstanceOf[js.Any],
139 | "min" -> min,
140 | "pattern" -> pattern,
141 | "required" -> required,
142 | "transform" -> transform,
143 | "type" -> `type`,
144 | "whitespace" -> whitespace
145 | ).asInstanceOf[ValidationRules]
146 | }
147 |
148 | @js.native
149 | trait FieldDecoratorOptions extends js.Object {
150 | val getValueFromEvent: UndefOr[Event => js.Object] = js.native
151 | val getValueProps: UndefOr[Value => js.Object] = js.native
152 | val initialValue: UndefOr[js.Object] = js.native
153 | val normalize: UndefOr[(Value, Value, Value) => js.Object] = js.native
154 | val preserve: UndefOr[Boolean] = js.native
155 | val rules: UndefOr[js.Array[ValidationRules]] = js.native
156 | val validateFirst: Boolean = js.native
157 | val trigger: String = js.native
158 | val validateTrigger: String | js.Array[String] = js.native
159 | val valuePropName: String = js.native
160 | }
161 |
162 | object FieldDecoratorOptions {
163 | def apply(getValueFromEvent: UndefOr[Event => js.Object] = js.undefined,
164 | getValueProps: UndefOr[Value => js.Object] = js.undefined,
165 | initialValue: UndefOr[js.Object] = js.undefined,
166 | normalize: UndefOr[(Value, Value, Value) => js.Object] = js.undefined,
167 | preserve: UndefOr[Boolean] = js.undefined,
168 | rules: UndefOr[Seq[ValidationRules]] = js.undefined,
169 | validateFirst: Boolean = false,
170 | trigger: String = "onChange",
171 | validateTrigger: String | Seq[String] = "onChange",
172 | valuePropName: String = "value") =
173 | js.Dynamic.literal(
174 | getValueFromEvent = getValueFromEvent,
175 | getValueProps = getValueProps,
176 | initialValue = initialValue,
177 | normalize = normalize,
178 | preserve = preserve,
179 | rules = rules.map(_.toJSArray),
180 | validateFirst = validateFirst,
181 | trigger = trigger,
182 | validateTrigger = validateTrigger.asInstanceOf[js.Any],
183 | valuePropName = valuePropName
184 | ).asInstanceOf[FieldDecoratorOptions]
185 | }
186 |
187 | @react object Form extends ExternalComponent {
188 |
189 | case class Props(hideRequiredMark: Boolean = false,
190 | layout: String = "horizontal",
191 | onSubmit: UndefOr[Event => Unit] = js.undefined)
192 |
193 | override val component = AntForm.Form
194 |
195 | }
196 |
197 | @js.native
198 | trait FormOps extends js.Object {
199 |
200 | def getFieldDecorator(id: String, options: FieldDecoratorOptions): js.Function1[ReactElement, ReactElement] = js.native
201 |
202 | def getFieldError(name: String): String = js.native
203 |
204 | def getFieldsError(names: js.Array[String]): js.Dictionary[js.Array[String]] = js.native
205 |
206 | def getFieldsError(): js.Dictionary[js.Array[String]] = js.native
207 |
208 | def getFieldsValue(names: js.Array[String]): js.Dictionary[Value] = js.native
209 |
210 | def getFieldsValue(): js.Dictionary[Value] = js.native
211 |
212 | def isFieldsTouched(names: js.Array[String]): Boolean = js.native
213 |
214 | def isFieldTouched(name: String): Boolean = js.native
215 |
216 | def isFieldValidating(name: String): Boolean = js.native
217 |
218 | def resetFields(names: js.Array[String]): Unit = js.native
219 |
220 | def resetFields(): Unit = js.native
221 |
222 | def setFields(fields: js.Dictionary[FieldProperties]): Unit = js.native
223 |
224 | def setFieldsValue(fields: js.Dictionary[String]): Unit = js.native
225 |
226 | def validateFields(fieldNames: js.Dictionary[String],
227 | options: ValidationOptions,
228 | callback: js.Function2[js.Dictionary[ValidationErrorList], js.Dictionary[Value], Unit]): Unit = js.native
229 | def validateFields(callback: js.Function2[js.Dictionary[ValidationErrorList], js.Dictionary[Value], Unit]): Unit = js.native
230 |
231 | def validateFieldsAndScroll(fieldNames: js.Dictionary[String],
232 | options: ValidationOptions,
233 | callback: js.Function2[js.Dictionary[ValidationErrorList], js.Dictionary[Value], Unit]): Unit = js.native
234 | def validateFieldsAndScroll(callback: js.Function2[js.Dictionary[ValidationErrorList], js.Dictionary[Value], Unit]): Unit = js.native
235 |
236 | }
237 |
238 | @js.native
239 | trait FormStaticProps extends js.Object {
240 | def form: FormOps = js.native
241 | }
242 |
243 | @js.native
244 | trait StaticProps extends js.Object {
245 | def props: FormStaticProps = js.native
246 | }
247 |
248 | @react object FormItem extends ExternalComponent {
249 |
250 | case class Props(colon: Boolean = true,
251 | extra: UndefOr[String | ReactElement] = js.undefined,
252 | hasFeedback: Boolean = false,
253 | help: UndefOr[String | ReactElement] = js.undefined,
254 | label: UndefOr[String | ReactElement] = js.undefined,
255 | labelCol: UndefOr[ColProps] = js.undefined,
256 | required: Boolean = false,
257 | validateStatus: UndefOr[String] = js.undefined,
258 | wrapperCol: UndefOr[ColProps] = js.undefined)
259 |
260 | override val component = AntForm.Form.Item
261 | }
262 |
263 |
--------------------------------------------------------------------------------