Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e9d1f93723 | ||
|
|
d1360b2917 |
@@ -3,23 +3,25 @@ package controllers
|
|||||||
import actors.auctions.AuctionPublisher
|
import actors.auctions.AuctionPublisher
|
||||||
import controllers.authentication.AuthenticatedAction
|
import controllers.authentication.AuthenticatedAction
|
||||||
import dao.{AuctionsDAO, AuthTokensDAO}
|
import dao.{AuctionsDAO, AuthTokensDAO}
|
||||||
import models.AuctionStatus.Ready
|
|
||||||
import models.{Auction, User}
|
|
||||||
import org.apache.pekko.actor.typed.ActorRef
|
import org.apache.pekko.actor.typed.ActorRef
|
||||||
import play.api.libs.functional.syntax.toFunctionalBuilderOps
|
|
||||||
import play.api.libs.json.*
|
import play.api.libs.json.*
|
||||||
import play.api.mvc.{BaseController, ControllerComponents}
|
import play.api.mvc.{BaseController, ControllerComponents}
|
||||||
|
|
||||||
import javax.inject.{Inject, Singleton}
|
import javax.inject.{Inject, Singleton}
|
||||||
import scala.concurrent.{ExecutionContext, Future}
|
import scala.concurrent.{ExecutionContext, Future}
|
||||||
|
import models.Auction
|
||||||
|
import caching.{CheckFresh, DigestEtagable, Etagable}
|
||||||
|
|
||||||
|
import java.io.DataOutputStream
|
||||||
|
|
||||||
@Singleton
|
@Singleton
|
||||||
class AuctionsController @Inject()(val controllerComponents: ControllerComponents,
|
class AuctionsController @Inject()(val controllerComponents: ControllerComponents,
|
||||||
val auctionsDAO: AuctionsDAO,
|
val auctionsDAO: AuctionsDAO,
|
||||||
val authTokensDAO: AuthTokensDAO,
|
val authTokensDAO: AuthTokensDAO,
|
||||||
authenticatedAction: AuthenticatedAction,
|
authenticatedAction: AuthenticatedAction,
|
||||||
val auctionPublisher: ActorRef[AuctionPublisher.Command],
|
val auctionPublisher: ActorRef[AuctionPublisher.Command])
|
||||||
implicit val ec: ExecutionContext) extends BaseController {
|
(implicit val ec: ExecutionContext)
|
||||||
|
extends BaseController with CheckFresh {
|
||||||
def index = authenticatedAction.async { request =>
|
def index = authenticatedAction.async { request =>
|
||||||
for {
|
for {
|
||||||
auctions <- auctionsDAO.list_available(request.user)
|
auctions <- auctionsDAO.list_available(request.user)
|
||||||
@@ -27,6 +29,16 @@ class AuctionsController @Inject()(val controllerComponents: ControllerComponent
|
|||||||
} yield Ok(json)
|
} yield Ok(json)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def show(id: Long) = authenticatedAction.async { implicit request =>
|
||||||
|
auctionsDAO.showAvailable(request.user, id).map { auctionOpt =>
|
||||||
|
auctionOpt.fold(NotFound) { auction =>
|
||||||
|
checkFresh(auction) {
|
||||||
|
Ok(Json.toJson(auction)).withHeaders("etag" -> auction.etag)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
def create = authenticatedAction.async(parse.json) { implicit request =>
|
def create = authenticatedAction.async(parse.json) { implicit request =>
|
||||||
val auctionCreateDataResult = request.body.validate[AuctionCreateData]
|
val auctionCreateDataResult = request.body.validate[AuctionCreateData]
|
||||||
auctionCreateDataResult.fold(
|
auctionCreateDataResult.fold(
|
||||||
@@ -44,4 +56,18 @@ class AuctionsController @Inject()(val controllerComponents: ControllerComponent
|
|||||||
private case class AuctionCreateData(name: String, startingPrice: BigDecimal)
|
private case class AuctionCreateData(name: String, startingPrice: BigDecimal)
|
||||||
|
|
||||||
private given Reads[AuctionCreateData] = Json.reads[AuctionCreateData]
|
private given Reads[AuctionCreateData] = Json.reads[AuctionCreateData]
|
||||||
|
|
||||||
|
private given DigestEtagable[Auction] with {
|
||||||
|
override protected def writeDigestData(a: Auction, s: DataOutputStream): Unit = {
|
||||||
|
s.writeLong(a.id.getOrElse(0))
|
||||||
|
s.writeLong(a.authorId)
|
||||||
|
s.writeUTF(a.gid)
|
||||||
|
s.writeUTF(a.name)
|
||||||
|
s.writeLong(a.createdAt.getEpochSecond)
|
||||||
|
|
||||||
|
s.writeLong(a.currentBidderId.getOrElse(0))
|
||||||
|
a.currentBid.foreach(b => s.writeLong((b * 1000).toLong))
|
||||||
|
a.currentBidAt.foreach(ba => s.writeLong(ba.getEpochSecond))
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,17 +1,15 @@
|
|||||||
package controllers
|
package controllers
|
||||||
|
|
||||||
import javax.inject.*
|
|
||||||
import play.api.*
|
import play.api.*
|
||||||
import play.api.mvc.*
|
import _root_.controllers.authentication.{AuthenticatedAction, AuthenticatedRequest}
|
||||||
import authentication.{AuthenticatedAction, AuthenticatedRequest}
|
import _root_.controllers.caching.{CheckFresh, DigestEtagable}
|
||||||
import caching.{Etagable, CheckFresh}
|
|
||||||
import models.User
|
import models.User
|
||||||
import play.api.libs.json.Json
|
import play.api.libs.json.Json
|
||||||
|
import play.api.mvc.*
|
||||||
|
|
||||||
import java.nio.ByteBuffer
|
import java.io.DataOutputStream
|
||||||
import java.nio.charset.StandardCharsets.UTF_8
|
import java.nio.charset.StandardCharsets.UTF_8
|
||||||
import java.security.MessageDigest
|
import javax.inject.*
|
||||||
import java.util.Base64
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This controller creates an `Action` to handle HTTP requests to the
|
* This controller creates an `Action` to handle HTTP requests to the
|
||||||
@@ -37,21 +35,15 @@ class HomeController @Inject()(val controllerComponents: ControllerComponents,
|
|||||||
val user = request.user
|
val user = request.user
|
||||||
checkFresh(user) {
|
checkFresh(user) {
|
||||||
// TODO: put the max-age somewhere into config
|
// TODO: put the max-age somewhere into config
|
||||||
Ok(Json.toJson(user)).withHeaders("Etag" -> user.etag, "Cache-Control" -> "private, max-age=15, must-revalidate")
|
Ok(Json.toJson(user)).withHeaders("Cache-Control" -> "private, max-age=15, must-revalidate")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private given Etagable[models.User] with {
|
private given DigestEtagable[models.User] with {
|
||||||
private val base64 = Base64.getUrlEncoder
|
override protected def writeDigestData(a: User, s: DataOutputStream): Unit = {
|
||||||
|
s.writeLong(a.id.getOrElse(0))
|
||||||
extension (a: User)
|
s.write(a.gid.getBytes(UTF_8))
|
||||||
override def etag: String = {
|
s.write(a.username.getBytes(UTF_8))
|
||||||
val md = MessageDigest.getInstance("SHA-1")
|
|
||||||
md.update(ByteBuffer.allocate(8).putLong(a.id.getOrElse(0)))
|
|
||||||
md.update(a.gid.getBytes(UTF_8))
|
|
||||||
md.update(a.username.getBytes(UTF_8))
|
|
||||||
|
|
||||||
base64.encodeToString(md.digest())
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,24 +3,55 @@ package controllers.caching
|
|||||||
import play.api.mvc.Results.NotModified
|
import play.api.mvc.Results.NotModified
|
||||||
import play.api.mvc.{Request, Result}
|
import play.api.mvc.{Request, Result}
|
||||||
|
|
||||||
|
import java.io.{ByteArrayOutputStream, DataOutputStream, OutputStream}
|
||||||
|
import java.security.{DigestOutputStream, MessageDigest}
|
||||||
|
import java.util.Base64
|
||||||
import scala.concurrent.{ExecutionContext, Future}
|
import scala.concurrent.{ExecutionContext, Future}
|
||||||
|
import scala.util.Using
|
||||||
|
|
||||||
trait Etagable[A]:
|
trait Etagable[A]:
|
||||||
extension (a: A) def etag: String
|
extension (a: A) def etag: String
|
||||||
|
|
||||||
|
|
||||||
|
trait DigestEtagable[A] extends Etagable[A]:
|
||||||
|
private val base64 = Base64.getUrlEncoder.withoutPadding
|
||||||
|
|
||||||
|
protected def writeDigestData(a: A, s: DataOutputStream): Unit
|
||||||
|
|
||||||
|
extension (a: A) {
|
||||||
|
override def etag: String = {
|
||||||
|
val md = MessageDigest.getInstance("SHA-1")
|
||||||
|
Using(DataOutputStream(DigestOutputStream(OutputStream.nullOutputStream(), md))) { dos =>
|
||||||
|
writeDigestData(a, dos)
|
||||||
|
dos.flush()
|
||||||
|
}
|
||||||
|
|
||||||
|
base64.encodeToString(md.digest())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
trait CheckFresh {
|
trait CheckFresh {
|
||||||
protected def checkFreshAsync[A, E: Etagable](entity: E)(onStale: => Future[Result])(using request: Request[A], ec: ExecutionContext): Future[Result] = {
|
protected def checkFreshAsync[A, E: Etagable](entity: E)(onStale: => Future[Result])(using request: Request[A], ec: ExecutionContext): Future[Result] = {
|
||||||
compareEtag(request, entity).fold(onStale)(
|
compareEtag(request, entity).fold(
|
||||||
etag => Future.successful(NotModified.withHeaders("ETag" -> etag))
|
entityEtag => onStale.map(_.withHeaders("etag" -> entityEtag)),
|
||||||
|
requestEtag => Future.successful(notModified(requestEtag))
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
protected def checkFresh[A, E: Etagable](entity: E)(onStale: => Result)(using request: Request[A]): Result = {
|
protected def checkFresh[A, E: Etagable](entity: E)(onStale: => Result)(using request: Request[A]): Result = {
|
||||||
compareEtag(request, entity).fold(onStale)(
|
compareEtag(request, entity).fold(
|
||||||
etag => NotModified.withHeaders("ETag" -> etag)
|
entityEtag => onStale.withHeaders("etag" -> entityEtag),
|
||||||
|
requestEtag => notModified(requestEtag)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private def compareEtag[A, E: Etagable](request: Request[A], entity: E) = request.headers.get("If-None-Match").filter(e => e == entity.etag)
|
private def compareEtag[A, E: Etagable](request: Request[A], entity: E) = {
|
||||||
|
val entityEtag = entity.etag
|
||||||
|
request.headers.get("If-None-Match").filter(e => e == entityEtag) match {
|
||||||
|
case Some(etag) => Right(etag)
|
||||||
|
case None => Left(entityEtag)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
private def notModified(etag: String) = NotModified.withHeaders("etag" -> etag)
|
||||||
}
|
}
|
||||||
@@ -1,31 +1,35 @@
|
|||||||
package dao
|
package dao
|
||||||
|
|
||||||
import models.{Auction, AuctionStatus}
|
|
||||||
import schemas.Auctions
|
|
||||||
import schemas.given
|
|
||||||
import play.api.db.slick.{DatabaseConfigProvider, HasDatabaseConfigProvider}
|
import play.api.db.slick.{DatabaseConfigProvider, HasDatabaseConfigProvider}
|
||||||
import slick.jdbc.PostgresProfile
|
import slick.jdbc.PostgresProfile
|
||||||
import slick.jdbc.PostgresProfile.api.*
|
import slick.jdbc.PostgresProfile.api.*
|
||||||
|
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import scala.concurrent.{ExecutionContext, Future}
|
import scala.concurrent.{ExecutionContext, Future}
|
||||||
import scala.util.Random
|
import scala.util.Random
|
||||||
|
|
||||||
|
import models.{Auction, AuctionStatus, User}
|
||||||
|
import schemas.Auctions
|
||||||
|
import schemas.given
|
||||||
|
|
||||||
class AuctionsDAO @Inject()(protected val dbConfigProvider: DatabaseConfigProvider)(implicit val ec: ExecutionContext)
|
class AuctionsDAO @Inject()(protected val dbConfigProvider: DatabaseConfigProvider)(implicit val ec: ExecutionContext)
|
||||||
extends HasDatabaseConfigProvider[PostgresProfile] {
|
extends HasDatabaseConfigProvider[PostgresProfile] {
|
||||||
|
|
||||||
def list_available(user: models.User): Future[Seq[Auction]] = {
|
def list_available(user: User): Future[Seq[Auction]] = {
|
||||||
|
db.run(availableScope(user).result)
|
||||||
|
}
|
||||||
|
|
||||||
|
def showAvailable(user: User, id: Long): Future[Option[Auction]] = {
|
||||||
db.run {
|
db.run {
|
||||||
Auctions.filter(row => (row.status === AuctionStatus.Ready) && (row.authorId =!= user.id.get)).result
|
availableScope(user).filter(_.id === id).take(1).result.headOption
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
def list_created_by(user: models.User): Future[Seq[Auction]] = {
|
def list_created_by(user: User): Future[Seq[Auction]] = {
|
||||||
db.run(Auctions.filter(_.authorId === user.id).result)
|
db.run(Auctions.filter(_.authorId === user.id).result)
|
||||||
}
|
}
|
||||||
|
|
||||||
def create(name: String, startingPrice: BigDecimal, author: models.User): Future[Auction] = {
|
def create(name: String, startingPrice: BigDecimal, author: User): Future[Auction] = {
|
||||||
val auction = Auction(
|
val auction = Auction(
|
||||||
id = None, name = name,
|
id = None, name = name,
|
||||||
gid = Random.alphanumeric.take(24).mkString,
|
gid = Random.alphanumeric.take(24).mkString,
|
||||||
@@ -47,4 +51,8 @@ class AuctionsDAO @Inject()(protected val dbConfigProvider: DatabaseConfigProvid
|
|||||||
db.run(Auctions.filter(_.id === auctionId).map(_.status).update(status))
|
db.run(Auctions.filter(_.id === auctionId).map(_.status).update(status))
|
||||||
.map(_ == 1)
|
.map(_ == 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private def availableScope(user: User) = {
|
||||||
|
Auctions.filter(row => (row.status === AuctionStatus.Ready) && (row.authorId =!= user.id.get))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ GET /user_data controllers.HomeController.userData()
|
|||||||
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()
|
GET /auctions controllers.AuctionsController.index()
|
||||||
|
GET /auctions/:id controllers.AuctionsController.show(id: Long)
|
||||||
POST /auctions controllers.AuctionsController.create()
|
POST /auctions controllers.AuctionsController.create()
|
||||||
|
|
||||||
-> /-devel controllers.devel.DevRouter
|
-> /-devel controllers.devel.DevRouter
|
||||||
Reference in New Issue
Block a user