Implemented login
This commit is contained in:
@@ -0,0 +1,43 @@
|
|||||||
|
package controllers.authentication
|
||||||
|
|
||||||
|
import dao.{AuthTokensDAO, UsersDAO}
|
||||||
|
import jakarta.inject.{Inject, Singleton}
|
||||||
|
import models.AuthToken
|
||||||
|
import play.api.libs.json.*
|
||||||
|
import play.api.libs.functional.syntax.*
|
||||||
|
import play.api.mvc.{AbstractController, ControllerComponents}
|
||||||
|
|
||||||
|
import scala.concurrent.{ExecutionContext, Future}
|
||||||
|
|
||||||
|
case class LoginData(username: String, password: String)
|
||||||
|
implicit val loginDataReads: Reads[LoginData] = (
|
||||||
|
(JsPath \ "username").read[String] and
|
||||||
|
(JsPath \ "password").read[String]
|
||||||
|
)(LoginData.apply)
|
||||||
|
implicit val tokenWrites: Writes[AuthToken] = (token: AuthToken) => Json.obj(
|
||||||
|
"token" -> token.token,
|
||||||
|
"createdAt" -> token.createdAt,
|
||||||
|
"expiresAt" -> token.expiresAt
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@Singleton
|
||||||
|
class SessionsController @Inject()(controllerComponents: ControllerComponents, usersDAO: UsersDAO, authTokensDAO: AuthTokensDAO, implicit val ec: ExecutionContext) extends AbstractController(controllerComponents) {
|
||||||
|
def login() = Action.async(parse.json) { implicit request =>
|
||||||
|
val loginDataResult = request.body.validate[LoginData]
|
||||||
|
loginDataResult.fold(
|
||||||
|
errors => {
|
||||||
|
Future.successful(BadRequest(Json.obj("message" -> JsError.toJson(errors))))
|
||||||
|
},
|
||||||
|
loginData => {
|
||||||
|
usersDAO.authenticate(loginData.username, loginData.password).flatMap { userOption =>
|
||||||
|
userOption.fold(
|
||||||
|
Future.successful(UnprocessableEntity(Json.obj("message" -> "Invalid authentication")))
|
||||||
|
){ user =>
|
||||||
|
authTokensDAO.createToken(user).map(token => Ok(Json.toJson(token)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
package dao
|
||||||
|
|
||||||
|
import jakarta.inject.Inject
|
||||||
|
import play.api.db.slick.{DatabaseConfigProvider, HasDatabaseConfigProvider}
|
||||||
|
import slick.jdbc.PostgresProfile
|
||||||
|
import slick.jdbc.PostgresProfile.api._
|
||||||
|
|
||||||
|
import java.nio.ByteBuffer
|
||||||
|
import java.security.SecureRandom
|
||||||
|
import java.util.Base64
|
||||||
|
import java.time.{Instant, Duration}
|
||||||
|
|
||||||
|
import scala.concurrent.{ExecutionContext, Future}
|
||||||
|
import models.AuthToken
|
||||||
|
import schemas.AuthTokens
|
||||||
|
|
||||||
|
class AuthTokensDAO @Inject()(protected val dbConfigProvider: DatabaseConfigProvider)(implicit val ec: ExecutionContext) extends HasDatabaseConfigProvider[PostgresProfile] {
|
||||||
|
private lazy val random = SecureRandom()
|
||||||
|
private lazy val base64 = Base64.getEncoder
|
||||||
|
private final lazy val tokenValidDuration = Duration.ofDays(30)
|
||||||
|
|
||||||
|
def createToken(user: models.User): Future[AuthToken] = {
|
||||||
|
val tokenRaw = ByteBuffer.allocate(10)
|
||||||
|
random.nextBytes(tokenRaw.array())
|
||||||
|
val token = AuthToken(
|
||||||
|
id = None,
|
||||||
|
userId = user.id.get,
|
||||||
|
token = base64.encodeToString(tokenRaw.array()),
|
||||||
|
createdAt = Instant.now,
|
||||||
|
expiresAt = Instant.now.plus(tokenValidDuration)
|
||||||
|
)
|
||||||
|
|
||||||
|
db.run((AuthTokens returning AuthTokens.map(_.id)) += token).map { tokenId => token.copy(id = Some(tokenId)) }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
package dao
|
||||||
|
|
||||||
|
import de.mkammerer.argon2.Argon2Factory
|
||||||
|
import execution.CryptoExecutionContext
|
||||||
|
import jakarta.inject.Inject
|
||||||
|
import play.api.db.slick.{DatabaseConfigProvider, HasDatabaseConfigProvider}
|
||||||
|
import slick.jdbc.PostgresProfile
|
||||||
|
import slick.jdbc.PostgresProfile.api.*
|
||||||
|
|
||||||
|
import scala.concurrent.{ExecutionContext, Future}
|
||||||
|
import models.User
|
||||||
|
import schemas.Users
|
||||||
|
|
||||||
|
class UsersDAO @Inject()(protected val dbConfigProvider: DatabaseConfigProvider, private val cryptoEC: CryptoExecutionContext)(implicit val ec: ExecutionContext)
|
||||||
|
extends HasDatabaseConfigProvider[PostgresProfile] {
|
||||||
|
private val argon2 = Argon2Factory.create()
|
||||||
|
|
||||||
|
def authenticate(username: String, password: String): Future[Option[User]] = {
|
||||||
|
val query = Users.filter(_.username === username).take(1).result.headOption
|
||||||
|
|
||||||
|
val userFuture = db.run(query)
|
||||||
|
|
||||||
|
userFuture.map { userOption =>
|
||||||
|
for {
|
||||||
|
user <- userOption
|
||||||
|
digest <- user.passwordDigest
|
||||||
|
if argon2.verify(digest, password.getBytes("UTF-8"))
|
||||||
|
} yield user
|
||||||
|
}(using cryptoEC)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
package execution
|
||||||
|
|
||||||
|
import jakarta.inject.{Inject, Singleton}
|
||||||
|
import org.apache.pekko.actor.ActorSystem
|
||||||
|
import play.api.libs.concurrent.CustomExecutionContext
|
||||||
|
|
||||||
|
@Singleton
|
||||||
|
class CryptoExecutionContext @Inject()(system: ActorSystem) extends CustomExecutionContext(system, "pekko.actor.crypto-dispatcher")
|
||||||
@@ -18,6 +18,9 @@ libraryDependencies += "org.playframework" %% "play-slick" % "6.2.0"
|
|||||||
// Source: https://mvnrepository.com/artifact/org.playframework/play-slick-evolutions
|
// Source: https://mvnrepository.com/artifact/org.playframework/play-slick-evolutions
|
||||||
libraryDependencies += "org.playframework" %% "play-slick-evolutions" % "6.2.0"
|
libraryDependencies += "org.playframework" %% "play-slick-evolutions" % "6.2.0"
|
||||||
|
|
||||||
|
// Source: https://mvnrepository.com/artifact/de.mkammerer/argon2-jvm
|
||||||
|
libraryDependencies += "de.mkammerer" % "argon2-jvm" % "2.12"
|
||||||
|
|
||||||
// Adds additional packages into Twirl
|
// Adds additional packages into Twirl
|
||||||
//TwirlKeys.templateImports += "me.artemis.controllers._"
|
//TwirlKeys.templateImports += "me.artemis.controllers._"
|
||||||
|
|
||||||
|
|||||||
@@ -13,3 +13,16 @@ slick.dbs.default = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pekko {
|
||||||
|
actor {
|
||||||
|
crypto-dispatcher {
|
||||||
|
type = Dispatcher
|
||||||
|
executor = "thread-pool-executor"
|
||||||
|
thread-pool-executor {
|
||||||
|
fixed-pool-size = 2
|
||||||
|
}
|
||||||
|
throughput = 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,5 +6,7 @@
|
|||||||
# An example controller showing a sample home page
|
# An example controller showing a sample home page
|
||||||
GET / controllers.HomeController.index()
|
GET / controllers.HomeController.index()
|
||||||
|
|
||||||
|
POST /login controllers.authentication.SessionsController.login()
|
||||||
|
|
||||||
# Map static resources from the /public folder to the /assets URL path
|
# Map static resources from the /public folder to the /assets URL path
|
||||||
GET /assets/*file controllers.Assets.versioned(path="/public", file: Asset)
|
GET /assets/*file controllers.Assets.versioned(path="/public", file: Asset)
|
||||||
|
|||||||
Reference in New Issue
Block a user