From 621fcf90fa79c1470661a7b17147b23ed57155c7 Mon Sep 17 00:00:00 2001 From: Artemiy Solopov Date: Wed, 16 Sep 2026 22:55:31 +0300 Subject: [PATCH] Caching and user data endpoint --- app/controllers/HomeController.scala | 41 ++++++++++++++++++++++++--- app/controllers/caching/caching.scala | 26 +++++++++++++++++ app/models/models.scala | 10 +++---- conf/application.conf | 1 + conf/routes | 2 ++ 5 files changed, 70 insertions(+), 10 deletions(-) create mode 100644 app/controllers/caching/caching.scala diff --git a/app/controllers/HomeController.scala b/app/controllers/HomeController.scala index 272ed2d..5315e7c 100644 --- a/app/controllers/HomeController.scala +++ b/app/controllers/HomeController.scala @@ -1,15 +1,26 @@ package controllers -import javax.inject._ -import play.api._ -import play.api.mvc._ +import javax.inject.* +import play.api.* +import play.api.mvc.* +import authentication.{AuthenticatedAction, AuthenticatedRequest} +import caching.{Etagable, checkFresh} +import models.User +import play.api.libs.json.Json + +import java.nio.ByteBuffer +import java.nio.charset.StandardCharsets.UTF_8 +import java.security.MessageDigest +import java.util.Base64 /** * This controller creates an `Action` to handle HTTP requests to the * application's home page. */ @Singleton -class HomeController @Inject()(val controllerComponents: ControllerComponents) extends BaseController { +class HomeController @Inject()(val controllerComponents: ControllerComponents, + authenticatedAction: AuthenticatedAction) + extends BaseController with Logging { /** * Create an Action to render an HTML page. @@ -21,4 +32,26 @@ class HomeController @Inject()(val controllerComponents: ControllerComponents) e def index() = Action { implicit request: Request[AnyContent] => Ok(views.html.index()) } + + def userData = authenticatedAction { implicit request: AuthenticatedRequest[AnyContent] => + val user = request.user + request.checkFresh(user) { + // TODO: put the max-age somewhere into config + Ok(Json.toJson(user)).withHeaders("Etag" -> user.etag, "Cache-Control" -> "private, max-age=15, must-revalidate") + } + } + + private given Etagable[models.User] with { + private val base64 = Base64.getUrlEncoder + + extension (a: User) + override def etag: String = { + 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()) + } + } } diff --git a/app/controllers/caching/caching.scala b/app/controllers/caching/caching.scala new file mode 100644 index 0000000..be1518c --- /dev/null +++ b/app/controllers/caching/caching.scala @@ -0,0 +1,26 @@ +package controllers.caching + +import play.api.mvc.Results.NotModified +import play.api.mvc.{Request, Result} + +import scala.concurrent.{ExecutionContext, Future} + +trait Etagable[A]: + extension (a: A) def etag: String + + +extension [A, R[X] <: Request[X], E: Etagable](request: R[A]) { + def checkFreshAsync(entity: E)(onStale: => Future[Result])(using ec: ExecutionContext): Future[Result] = { + compareEtag(entity).fold(onStale)( + etag => Future.successful(NotModified.withHeaders("ETag" -> etag)) + ) + } + + def checkFresh(entity: E)(onStale: => Result): Result = { + compareEtag(entity).fold(onStale)( + etag => NotModified.withHeaders("ETag" -> etag) + ) + } + + private def compareEtag(entity: E) = request.headers.get("If-None-Match").filter(e => e == entity.etag) +} \ No newline at end of file diff --git a/app/models/models.scala b/app/models/models.scala index a1d43e6..01f4b25 100644 --- a/app/models/models.scala +++ b/app/models/models.scala @@ -6,12 +6,10 @@ import java.time.Instant case class User(id: Option[Long], 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 - ) - } + given writes: Writes[User] = (o: User) => Json.obj( + "gid" -> o.gid, + "username" -> o.username + ) } case class AuthToken(id: Option[Long], userId: Long, token: String, createdAt: Instant, expiresAt: Instant) diff --git a/conf/application.conf b/conf/application.conf index 54e0d8d..3326b89 100644 --- a/conf/application.conf +++ b/conf/application.conf @@ -8,6 +8,7 @@ play.filters.csrf.header.bypassHeaders { } play.filters.cors { allowedOrigins = ["http://localhost:4321"] + exposedHeaders = ["ETag"] } play.modules.enabled += "mqttClient.Module" diff --git a/conf/routes b/conf/routes index 726a52d..20c586e 100644 --- a/conf/routes +++ b/conf/routes @@ -9,6 +9,8 @@ GET / controllers.HomeController.index() POST /login controllers.authentication.SessionsController.login() DELETE /logout controllers.authentication.SessionsController.logout() +GET /user_data controllers.HomeController.userData() + # Map static resources from the /public folder to the /assets URL path GET /assets/*file controllers.Assets.versioned(path="/public", file: Asset)