Dev actions controller/router + user registration
This commit is contained in:
@@ -0,0 +1,32 @@
|
|||||||
|
package controllers.devel
|
||||||
|
|
||||||
|
import dao.UsersDAO
|
||||||
|
import play.api.libs.functional.syntax.toFunctionalBuilderOps
|
||||||
|
import play.api.libs.json.{JsError, JsPath, Json, Reads}
|
||||||
|
import play.api.mvc.*
|
||||||
|
|
||||||
|
import javax.inject.Inject
|
||||||
|
import scala.concurrent.{ExecutionContext, Future}
|
||||||
|
|
||||||
|
class DevActionsController @Inject()(val controllerComponents: ControllerComponents,
|
||||||
|
val usersDAO: UsersDAO)
|
||||||
|
(implicit val ec: ExecutionContext)
|
||||||
|
extends BaseController {
|
||||||
|
|
||||||
|
private case class UserData(username: String, password: String)
|
||||||
|
private implicit val userDataReads: Reads[UserData] = (
|
||||||
|
(JsPath \ "username").read[String] and (JsPath \ "password").read[String]
|
||||||
|
)(UserData.apply)
|
||||||
|
|
||||||
|
def createUser = Action.async(parse.json) { implicit request =>
|
||||||
|
request.body.validate[UserData].fold(
|
||||||
|
errors => Future.successful(BadRequest(JsError.toJson(errors))),
|
||||||
|
userData => usersDAO.create(userData.username, userData.password)
|
||||||
|
.map {
|
||||||
|
case Left(UsersDAO.UsernameTaken) => BadRequest(Json.obj("error" -> "Username exists"))
|
||||||
|
case Left(_ex) => BadRequest(Json.obj("error" -> "User creation error"))
|
||||||
|
case Right(user) => Ok(Json.toJson(user))
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
package controllers.devel
|
||||||
|
|
||||||
|
import play.api.Environment
|
||||||
|
import play.api.Mode.Dev
|
||||||
|
import play.api.mvc.*
|
||||||
|
import play.api.routing.Router.Routes
|
||||||
|
import play.api.routing.SimpleRouter
|
||||||
|
import play.api.routing.sird.{POST, UrlContext}
|
||||||
|
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
class DevRouter @Inject()(environment: Environment, controller: DevActionsController) extends SimpleRouter {
|
||||||
|
override def routes: Routes = {
|
||||||
|
if (environment.mode == Dev) {
|
||||||
|
devRoutes
|
||||||
|
} else {
|
||||||
|
PartialFunction.empty
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val devRoutes: PartialFunction[RequestHeader, Handler] = {
|
||||||
|
case POST(p"/users") => controller.createUser()
|
||||||
|
}
|
||||||
|
}
|
||||||
+53
-5
@@ -1,5 +1,6 @@
|
|||||||
package dao
|
package dao
|
||||||
|
|
||||||
|
import dao.UsersDAO.{UnexpectedError, UsernameTaken}
|
||||||
import de.mkammerer.argon2.Argon2Factory
|
import de.mkammerer.argon2.Argon2Factory
|
||||||
import execution.CryptoExecutionContext
|
import execution.CryptoExecutionContext
|
||||||
import jakarta.inject.Inject
|
import jakarta.inject.Inject
|
||||||
@@ -9,23 +10,70 @@ import slick.jdbc.PostgresProfile.api.*
|
|||||||
|
|
||||||
import scala.concurrent.{ExecutionContext, Future}
|
import scala.concurrent.{ExecutionContext, Future}
|
||||||
import models.User
|
import models.User
|
||||||
|
import org.postgresql.util.{PSQLException, PSQLState}
|
||||||
import schemas.Users
|
import schemas.Users
|
||||||
|
|
||||||
class UsersDAO @Inject()(protected val dbConfigProvider: DatabaseConfigProvider, private val cryptoEC: CryptoExecutionContext)(implicit val ec: ExecutionContext)
|
import java.nio.charset.StandardCharsets.UTF_8
|
||||||
|
import scala.util.Random
|
||||||
|
|
||||||
|
class UsersDAO @Inject()(protected val dbConfigProvider: DatabaseConfigProvider, private val cryptoEC: CryptoExecutionContext)
|
||||||
|
(using ec: ExecutionContext)
|
||||||
extends HasDatabaseConfigProvider[PostgresProfile] {
|
extends HasDatabaseConfigProvider[PostgresProfile] {
|
||||||
private val argon2 = Argon2Factory.create()
|
private val argon2 = Argon2Factory.create()
|
||||||
|
|
||||||
|
private object ArgonParameters {
|
||||||
|
val iterations = 3
|
||||||
|
val parallelism = 1
|
||||||
|
val memory = 65536
|
||||||
|
}
|
||||||
|
|
||||||
|
private final val dummyDigest = "$argon2i$v=19$m=65536,t=3,p=1$EB9QibLwzqkO7ne2bJoU/Q$ADkdyweXKylJORvWXbBVekURYaazkW/OplTq2wdN1So"
|
||||||
|
|
||||||
def authenticate(username: String, password: String): Future[Option[User]] = {
|
def authenticate(username: String, password: String): Future[Option[User]] = {
|
||||||
val query = Users.filter(_.username === username).take(1).result.headOption
|
val query = Users.filter(_.username === username).take(1).result.headOption
|
||||||
|
|
||||||
val userFuture = db.run(query)
|
val userFuture = db.run(query)
|
||||||
|
|
||||||
userFuture.map { userOption =>
|
userFuture.map { userOption =>
|
||||||
for {
|
for {
|
||||||
user <- userOption
|
user <- userOption
|
||||||
digest <- user.passwordDigest
|
digest <- user.passwordDigest
|
||||||
if argon2.verify(digest, password.getBytes("UTF-8"))
|
} yield (user, digest)
|
||||||
} yield user
|
}.map {
|
||||||
}(using cryptoEC)
|
case Some((user, digest)) => (Some(user), digest)
|
||||||
|
case None => (None, dummyDigest)
|
||||||
|
}.flatMap { (userOpt, digest) =>
|
||||||
|
for {
|
||||||
|
validHash <- Future(argon2.verify(digest, password.getBytes(UTF_8)))(using cryptoEC)
|
||||||
|
opt = userOpt.flatMap(u => if validHash then Some(u) else None)
|
||||||
|
} yield opt
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def create(username: String, password: String): Future[Either[UsersDAO.RegistrationError, User]] = {
|
||||||
|
Future(argonHash(password))(using cryptoEC)
|
||||||
|
.flatMap{ passwordDigest =>
|
||||||
|
val gid = Random.alphanumeric.take(12).mkString
|
||||||
|
val user = User(
|
||||||
|
id = None,
|
||||||
|
gid = gid,
|
||||||
|
username = username,
|
||||||
|
passwordDigest = Some(passwordDigest)
|
||||||
|
)
|
||||||
|
|
||||||
|
db.run((Users returning Users) += user).map(Right(_))
|
||||||
|
}.recover {
|
||||||
|
case ex: PSQLException if ex.getSQLState == PSQLState.UNIQUE_VIOLATION.getState =>
|
||||||
|
Left(UsernameTaken)
|
||||||
|
case ex: Throwable =>
|
||||||
|
Left(UnexpectedError(ex))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private def argonHash(data: String) = argon2.hash(ArgonParameters.iterations, ArgonParameters.memory, ArgonParameters.parallelism, data.getBytes(UTF_8))
|
||||||
|
}
|
||||||
|
|
||||||
|
object UsersDAO {
|
||||||
|
sealed trait RegistrationError
|
||||||
|
case object UsernameTaken extends RegistrationError
|
||||||
|
case class UnexpectedError(cause: Throwable) extends RegistrationError
|
||||||
|
}
|
||||||
@@ -5,6 +5,14 @@ import play.api.libs.json.{JsValue, Json, Writes}
|
|||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
|
|
||||||
case class User(id: Option[Int], username: String, gid: String, passwordDigest: Option[String])
|
case class User(id: Option[Int], username: String, gid: String, passwordDigest: Option[String])
|
||||||
|
object User {
|
||||||
|
given writes: Writes[User] = new Writes[User] {
|
||||||
|
override def writes(o: User): JsValue = Json.obj(
|
||||||
|
"gid" -> o.gid,
|
||||||
|
"username" -> o.username
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
case class AuthToken(id: Option[Int], userId: Int, token: String, createdAt: Instant, expiresAt: Instant)
|
case class AuthToken(id: Option[Int], userId: Int, token: String, createdAt: Instant, expiresAt: Instant)
|
||||||
|
|
||||||
@@ -28,6 +36,7 @@ object Auction {
|
|||||||
"id" -> a.id,
|
"id" -> a.id,
|
||||||
"name" -> a.name,
|
"name" -> a.name,
|
||||||
"gid" -> a.gid,
|
"gid" -> a.gid,
|
||||||
|
"status" -> a.status.toString,
|
||||||
"starting_price" -> a.startingPrice,
|
"starting_price" -> a.startingPrice,
|
||||||
"current_bid" -> a.currentBid,
|
"current_bid" -> a.currentBid,
|
||||||
"current_bidder_id" -> a.currentBidderId,
|
"current_bidder_id" -> a.currentBidderId,
|
||||||
|
|||||||
@@ -45,8 +45,8 @@ class AuctionsTable(tag: Tag) extends Table[Auction](tag, "auctions") {
|
|||||||
def status = column[AuctionStatus]("status")
|
def status = column[AuctionStatus]("status")
|
||||||
def gid = column[String]("gid", O.Unique)
|
def gid = column[String]("gid", O.Unique)
|
||||||
def authorId = column[Int]("author_id")
|
def authorId = column[Int]("author_id")
|
||||||
def startingPrice = column[BigDecimal]("starting_price")
|
def startingPrice = column[BigDecimal]("starting_price", O.SqlType("DECIMAL(20, 3)"))
|
||||||
def currentBid = column[Option[BigDecimal]]("current_bid")
|
def currentBid = column[Option[BigDecimal]]("current_bid", O.SqlType("DECIMAL(20, 3)"))
|
||||||
def currentBidderId = column[Option[Int]]("current_bidder_id")
|
def currentBidderId = column[Option[Int]]("current_bidder_id")
|
||||||
def currentBidAt = column[Option[Instant]]("current_bid_at")
|
def currentBidAt = column[Option[Instant]]("current_bid_at")
|
||||||
def createdAt = column[Instant]("created_at")
|
def createdAt = column[Instant]("created_at")
|
||||||
|
|||||||
@@ -12,4 +12,7 @@ DELETE /logout controllers.authentication.SessionsControlle
|
|||||||
# 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)
|
||||||
|
|
||||||
|
GET /auctions controllers.AuctionsController.index()
|
||||||
POST /auctions controllers.AuctionsController.create()
|
POST /auctions controllers.AuctionsController.create()
|
||||||
|
|
||||||
|
-> /-devel controllers.devel.DevRouter
|
||||||
Reference in New Issue
Block a user