Skip to content

Commit e3bb9fb

Browse files
committed
Added silhouette for authentication with a basic authentication flow
1 parent 9dbf66c commit e3bb9fb

31 files changed

+734
-105
lines changed

README.md

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,15 @@
11
# README #
22

3-
Seed project for Play framework + ScalaJS using Apollo client and Sangria.
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
413

514

615
## Development Guide
@@ -26,6 +35,12 @@ docker run --name=test_db_server -e 'MYSQL_ROOT_PASSWORD=root' -e 'MYSQL_ROOT_HO
2635
- web_client/Compile/managedSources: generates schema.graphql and query/mutation objects for the client
2736
- assembly: generates the uber jar
2837

38+
## Usage
39+
40+
The default login for the user interface is:
41+
- username: admin
42+
- password: admin
43+
2944
## Problems
3045

3146
- WebpackDevServer is not used at the moment

project/DatabaseUtils.scala

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ object DatabaseUtils {
1313
val slickOutputDir = settingKey[File]("The directory where the database mappings are outputted")
1414
val runEvolutions = taskKey[Seq[File]]("Runs the evolutions on the database")
1515

16-
def generateSlickTables(baseDir: File, outputDir: File, classPath: Classpath, runner: ScalaRun, stream: TaskStreams): Seq[File] = {
16+
def generateSlickTables(baseDir: File, outputDir: File, classPath: Seq[File], runner: ScalaRun, stream: TaskStreams): Seq[File] = {
1717
val config = ConfigFactory.parseFile(new File(s"${baseDir.getAbsolutePath}/conf/application.conf")).resolve()
1818
val url = config.getString("slick.dbs.default.db.url")
1919
val jdbcDriver = config.getString("slick.dbs.default.db.driver")
@@ -22,10 +22,10 @@ object DatabaseUtils {
2222
val user = config.getString("slick.dbs.default.db.user")
2323
val password = config.getString("slick.dbs.default.db.password")
2424
val ignoreInvalidDefaults = "true"
25-
val generatorClass = "slick.codegen.SourceCodeGenerator"
25+
val generatorClass = "infrastructure.JodaAwareSourceCodeGenerator"
2626
val outputMultipleFiles = "true"
2727
val generatorOptions = Array(slickProfile, jdbcDriver, url, outputDir.getPath, pkg, user, password, ignoreInvalidDefaults, generatorClass, outputMultipleFiles)
28-
runner.run("slick.codegen.SourceCodeGenerator", classPath.files, generatorOptions, stream.log).failed foreach (sys error _.getMessage)
28+
runner.run("slick.codegen.SourceCodeGenerator", classPath, generatorOptions, stream.log).failed foreach (sys error _.getMessage)
2929
stream.log.info("Written slick mappings in directory " + outputDir)
3030
(outputDir / pkg.replace('.', '/')).listFiles()
3131
}
@@ -58,7 +58,10 @@ object DatabaseUtils {
5858
val srcDir = (Compile / sourceDirectory).value
5959
val compilers = Keys.compilers.value
6060
val classPath = (Compile / dependencyClasspath).value
61-
val fileToCompile = srcDir / "infrastructure/ApplyEvolutions.scala"
61+
val filesToCompile = Seq(
62+
srcDir / "infrastructure/ApplyEvolutions.scala",
63+
srcDir / "infrastructure/JodaAwareSourceCodeGenerator.scala"
64+
)
6265
val outputDir = (Compile / classDirectory).value
6366
val resourcesDir = (Compile / resourceDirectory).value
6467
val options = Shared.compileOptions
@@ -69,7 +72,7 @@ object DatabaseUtils {
6972

7073
QuickCompiler.scalac(
7174
compilers,
72-
Seq(fileToCompile),
75+
filesToCompile,
7376
QuickCompiler.noChanges,
7477
classPath.map(_.data),
7578
outputDir,
@@ -99,7 +102,7 @@ object DatabaseUtils {
99102
runEvolutions.value
100103
val dir = slickOutputDir.value
101104
val baseDir = baseDirectory.value
102-
val classPath = (Compile / dependencyClasspath).value
105+
val classPath = (Compile / dependencyClasspath).value.files :+ (Compile / classDirectory).value
103106
val runner = (Compile / Keys.runner).value
104107
val stream = streams.value
105108
generateSlickTables(baseDir, dir, classPath, runner, stream)

project/ProjectVersionManager.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import sbt.Keys._
44
object ProjectVersionManager {
55
def writeConfig(projectId: String, projectName: String, projectPort: Int) = Def.task {
66
val content =s"""
7-
|package util
7+
|package version.util
88
|
99
|object Version {
1010
| val projectId = "$projectId"

project/Server.scala

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ object Server {
2020

2121
val playSlickV = "4.0.0"
2222
val slickV = "3.3.0"
23+
val silhouetteV = "6.0.0-SNAPSHOT"
2324

2425
private[this] val dependencies = {
2526
Seq(
@@ -31,7 +32,12 @@ object Server {
3132
"com.typesafe.play" %% "play-slick-evolutions" % playSlickV,
3233
"com.typesafe.slick" %% "slick" % slickV,
3334
"com.typesafe.slick" %% "slick-codegen" % slickV,
34-
"mysql" % "mysql-connector-java" % "6.0.6"
35+
"mysql" % "mysql-connector-java" % "6.0.6",
36+
"com.mohiva" %% "play-silhouette" % silhouetteV,
37+
"com.mohiva" %% "play-silhouette-password-bcrypt" % silhouetteV,
38+
"com.mohiva" %% "play-silhouette-crypto-jca" % silhouetteV,
39+
"com.mohiva" %% "play-silhouette-persistence" % silhouetteV,
40+
"com.github.tototoshi" %% "slick-joda-mapper" % "2.4.0"
3541
)
3642
}
3743

project/Shared.scala

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,10 @@ object Shared {
1818
private[this] val profilingEnabled = false
1919

2020
val compileOptions = Seq(
21-
"-target:jvm-1.8", "-encoding", "UTF-8", "-feature", "-deprecation", "-explaintypes", "-feature", "-unchecked",
21+
"-target:jvm-1.8", "-encoding", "UTF-8", "-feature", "-deprecation", "-explaintypes", "-unchecked",
2222
"–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"
23+
"-Yno-adapted-args", "-Ywarn-inaccessible", "-Ywarn-nullary-override", "-Ywarn-numeric-widen", "-Ywarn-infer-any",
24+
"-language:postfixOps"
2425
) ++ (if (profilingEnabled) {
2526
"-Ystatistics:typer" +: Seq("no-profiledb", "show-profiles", "generate-macro-flamegraph").map(s => s"-P:scalac-profiling:$s")
2627
} else { Nil })
@@ -51,7 +52,7 @@ object Shared {
5152
case PathList("META-INF", "io.netty.versions.properties") => MergeStrategy.first
5253
case PathList("sqlj", _ @ _*) => MergeStrategy.first
5354
case PathList("play", "reference-overrides.conf") => MergeStrategy.first
54-
case PathList("util", "Version$.class") => MergeStrategy.first
55+
case PathList("version","util", "Version$.class") => MergeStrategy.first
5556
case "module-info.class" => MergeStrategy.discard
5657
case "messages" => MergeStrategy.concat
5758
case "pom.xml" => MergeStrategy.discard

project/WebClient.scala

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import webscalajs.ScalaJSWeb
99

1010
object WebClient {
1111

12-
val slinkyVer = "0.5.2+6-10d43572"
12+
val slinkyVer = "0.6.0"
1313
val reactVer = "16.5.2"
1414

1515
/*
@@ -20,33 +20,37 @@ object WebClient {
2020
* 1. clone the repo https://github.com/apollographql/apollo-scalajs
2121
* 2. change the file "build.sbt" adding at its top the lines (backtick not included):
2222
* ```
23-
* version in ThisBuild := "0.6.1-SNAPSHOT"
23+
* version in ThisBuild := "0.7.1-SNAPSHOT"
2424
*
25-
* dynver in ThisBuild := "0.6.1-SNAPSHOT"
25+
* dynver in ThisBuild := "0.7.1-SNAPSHOT"
2626
*
2727
* ```
2828
* 3. run the command (backtick not included):
2929
* ```
3030
* sbt publishLocal
3131
* ```
3232
*/
33-
val apolloScalaJsVer = "0.6.1-SNAPSHOT"
33+
val apolloScalaJsVer = "0.7.1-SNAPSHOT"
3434

3535

3636
private[this] val clientSettings =
3737
Shared.commonSettings ++ WebpackUtils.wiringTasks ++ GraphqlUtils.generateTasks ++ Seq(
3838
scalaJSUseMainModuleInitializer := true, // Starts scalajs from a main function
3939
mainClass in Compile := Some("com.mypackage.Bootstrap"),
4040

41-
resolvers += "Apollo Bintray" at "https://dl.bintray.com/apollographql/maven/",
41+
resolvers ++= Seq(
42+
"Apollo Bintray" at "https://dl.bintray.com/apollographql/maven/",
43+
Resolver.bintrayRepo("hmil", "maven")
44+
),
4245

4346
libraryDependencies ++= Seq(
4447
"me.shadaj" %%% "slinky-web" % slinkyVer, // React DOM, HTML and SVG tags
4548
"me.shadaj" %%% "slinky-hot" % slinkyVer, // Hot loading, requires react-proxy package
4649
"me.shadaj" %%% "slinky-scalajsreact-interop" % slinkyVer, // Interop with japgolly/scalajs-react
4750
"com.apollographql" %%% "apollo-scalajs-core" % apolloScalaJsVer,
4851
"com.apollographql" %%% "apollo-scalajs-react" % apolloScalaJsVer,
49-
"org.sangria-graphql" %% "sangria-circe" % "1.1.0"
52+
"org.sangria-graphql" %% "sangria-circe" % "1.1.0",
53+
"fr.hmil" %%% "roshttp" % "2.2.4"
5054
),
5155
Compile / npmDependencies ++= Seq(
5256
"react" -> reactVer,
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
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+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
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+
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
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+
}

server/app/auth/User.scala

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
package auth
2+
3+
import com.mohiva.play.silhouette.api.Identity
4+
5+
case class User() extends Identity

0 commit comments

Comments
 (0)