From 64267a5f678fd8512b13e7ab236684abdd72e4df Mon Sep 17 00:00:00 2001 From: efim Date: Tue, 25 Apr 2023 09:55:45 +0400 Subject: [PATCH] backend - adding room service and wiring preparing to create a better auth module that uses room service --- .../sunshine/planningpoker/Auth.scala | 39 ++++++--- .../sunshine/planningpoker/BackendApp.scala | 16 ++-- .../planningpoker/MyHttpService.scala | 9 +-- .../sunshine/planningpoker/RoomService.scala | 43 ++++++++++ .../sunshine/planningpoker/Models.scala | 80 +++++++++++++++---- 5 files changed, 148 insertions(+), 39 deletions(-) create mode 100644 backend/src/main/scala/industries/sunshine/planningpoker/RoomService.scala diff --git a/backend/src/main/scala/industries/sunshine/planningpoker/Auth.scala b/backend/src/main/scala/industries/sunshine/planningpoker/Auth.scala index a579426..e3aa2a9 100644 --- a/backend/src/main/scala/industries/sunshine/planningpoker/Auth.scala +++ b/backend/src/main/scala/industries/sunshine/planningpoker/Auth.scala @@ -6,7 +6,8 @@ import cats.data.OptionT import org.http4s.Request import industries.sunshine.planningpoker.common.Models.PlayerID import java.util.UUID -import industries.sunshine.planningpoker.common.Models.RoomID +import industries.sunshine.planningpoker.common.Models.{RoomID, Room} +import org.http4s.ResponseCookie trait Auth { @@ -24,10 +25,10 @@ trait Auth { roomName: String, roomPassword: String, nickName: String - ): IO[Either[Unit, Long]] + ): IO[Either[Unit, ResponseCookie]] def leaveRoom( - sessionId: Long + sessionId: Long ): IO[Unit] } @@ -36,38 +37,56 @@ object Auth { def pureBadStub = new Auth { override def authUser = Kleisli((req: Request[IO]) => - OptionT.liftF(IO(println(s"authUser: $req")) >> IO(PlayerID(14) -> RoomID(101))) + OptionT.liftF( + IO(println(s"authUser: $req")) >> IO( + PlayerID(14) -> RoomID("testroom") + ) + ) ) override def accessRoom( roomName: String, roomPassword: String, nickName: String ) = - IO(println(s"> access room for $roomName $roomPassword $nickName, to return stub 111")) >> IO.pure(Right(111L)) + IO( + println( + s"> access room for $roomName $roomPassword $nickName, to return stub 111" + ) + ) >> IO.pure(Right(ResponseCookie("authcookie", "1"))) override def leaveRoom(sessionId: Long): IO[Unit] = IO(s"got request to leave for $sessionId") } type SessionsMap = Map[Long, (RoomID, PlayerID)] - val sessionsRef = Ref.of[IO, SessionsMap](Map.empty) - val roomPasswordsRef = Ref.of[IO, Map[Long, String]](Map.empty) + val sessionsRef = + Ref.of[IO, SessionsMap](Map(1L -> (RoomID("testroom"), PlayerID(1L)))) + val roomsRef = + Ref.of[IO, Map[RoomID, Room]](Map(RoomID("testroom") -> Room.testRoom)) def apply(): IO[Auth] = for { store <- sessionsRef - roomPasswords <- roomPasswordsRef + rooms <- roomsRef } yield new Auth { + + val authcookie = "authcookie" + override def authUser = Kleisli { (req: Request[IO]) => { - ??? + ??? // oh, this one for when cookie present } } + import cats.syntax.either._ + import cats.syntax.option._ override def accessRoom( roomName: String, roomPassword: String, nickName: String - ): IO[Either[Unit, Long]] = ??? + ): IO[Either[Unit, ResponseCookie]] = { + + ??? + } override def leaveRoom(sessionId: Long): IO[Unit] = ??? } diff --git a/backend/src/main/scala/industries/sunshine/planningpoker/BackendApp.scala b/backend/src/main/scala/industries/sunshine/planningpoker/BackendApp.scala index 98b36a7..9c0c73d 100644 --- a/backend/src/main/scala/industries/sunshine/planningpoker/BackendApp.scala +++ b/backend/src/main/scala/industries/sunshine/planningpoker/BackendApp.scala @@ -12,17 +12,21 @@ object BackendApp extends IOApp { val host = host"0.0.0.0" val port = port"8080" - val server = for { - srv <- EmberServerBuilder + val wiring = for { + roomService <- Resource.eval(RoomService.make[IO]) + httpService = MyHttpService.create(Auth.pureBadStub) + server <- EmberServerBuilder .default[IO] .withHost(host) .withPort(port) - .withHttpWebSocketApp(MyHttpService.create(Auth.pureBadStub)(_)) + .withHttpWebSocketApp(httpService(_)) .build - } yield srv + } yield server - server.use(server => - IO.delay(println(s"Server Has Started at ${server.address}")) >> IO.never.as(ExitCode.Success)) + wiring.use(server => + IO.delay(println(s"Server Has Started at ${server.address}")) >> IO.never + .as(ExitCode.Success) + ) } } diff --git a/backend/src/main/scala/industries/sunshine/planningpoker/MyHttpService.scala b/backend/src/main/scala/industries/sunshine/planningpoker/MyHttpService.scala index 1745c5a..6fa0208 100644 --- a/backend/src/main/scala/industries/sunshine/planningpoker/MyHttpService.scala +++ b/backend/src/main/scala/industries/sunshine/planningpoker/MyHttpService.scala @@ -62,12 +62,9 @@ object MyHttpService { resp <- authResult match { case Left(error) => Forbidden(error) - case Right(sessionId) => { - Ok("Logged in!").map( - _.addCookie( - ResponseCookie("authcookie", sessionId.toString()) - ) - ) + case Right(authCookie) => { + IO(println(s"> logging in ${data.nickname} to ${data.roomName}")) >> + Ok().map(_.addCookie(authCookie)) } } } yield resp diff --git a/backend/src/main/scala/industries/sunshine/planningpoker/RoomService.scala b/backend/src/main/scala/industries/sunshine/planningpoker/RoomService.scala new file mode 100644 index 0000000..04b6823 --- /dev/null +++ b/backend/src/main/scala/industries/sunshine/planningpoker/RoomService.scala @@ -0,0 +1,43 @@ +package industries.sunshine.planningpoker + +import industries.sunshine.planningpoker.common.Models.* +import cats.effect.{Ref, Sync} +import cats.syntax.all._ + +enum RoomError { + case RoomAlreadyExists(name: String) +} + +trait RoomService[F[_]] { + def createRoom(newRoom: Room): F[Either[RoomError, Room]] + def updateRoom(room: Room): F[Unit] + def deleteRoom(roomID: RoomID): F[Unit] + def getRoom(roomID: RoomID): F[Option[Room]] +} + +class InMemoryRoomService[F[_]: Sync](stateRef: Ref[F, Map[RoomID, Room]]) + extends RoomService[F] { + override def createRoom(newRoom: Room): F[Either[RoomError, Room]] = { + stateRef.modify { rooms => + rooms.get(newRoom.id) match { + case Some(_) => + rooms -> RoomError.RoomAlreadyExists(newRoom.id.name).asLeft[Room] + case None => + (rooms.updated(newRoom.id, newRoom)) -> newRoom.asRight[RoomError] + } + } + } + def updateRoom(room: Room): F[Unit] = stateRef.update { state => + state.get(room.id).fold(state)(oldRoom => state.updated(room.id, room)) + } + + def deleteRoom(roomID: RoomID): F[Unit] = stateRef.update(_.removed(roomID)) + + def getRoom(roomID: RoomID): F[Option[Room]] = stateRef.get.map(_.get(roomID)) + +} +object RoomService { + def make[F[_]: Sync]: F[RoomService[F]] = { + Ref.of[F, Map[RoomID, Room]](Map.empty).map(new InMemoryRoomService[F](_)) + } +} diff --git a/common/src/main/scala/industries/sunshine/planningpoker/Models.scala b/common/src/main/scala/industries/sunshine/planningpoker/Models.scala index 67cbbc3..13b068a 100644 --- a/common/src/main/scala/industries/sunshine/planningpoker/Models.scala +++ b/common/src/main/scala/industries/sunshine/planningpoker/Models.scala @@ -6,10 +6,14 @@ import io.circe._ object Models { /** view of the single planning poker round - * @param players - people who are currently playing - * @param allowedCards- the cards values that can be used by players - * @param round - state of the selected cards of the players - * @param canCloseRound - whether current player has access to button to finish the round + * @param players + * \- people who are currently playing + * @param allowedCards- + * the cards values that can be used by players + * @param round + * \- state of the selected cards of the players + * @param canCloseRound + * \- whether current player has access to button to finish the round */ final case class RoomStateView( players: List[Player], @@ -23,30 +27,72 @@ object Models { object RoomStateView { val empty = RoomStateView( - List.empty, PlayerID(0), List.empty, RoundState.Voting(None, List.empty), false + List.empty, + PlayerID(0), + List.empty, + RoundState.Voting(None, List.empty), + false ) } - enum RoundState derives Codec.AsObject: - /** view state for round before votes are open player can know their vote and - * who of the other players have voted - */ + /** view state for round before votes are open player can know their vote + * and who of the other players have voted + */ case Voting( - myCard: Option[String], - alreadyVoted: List[Player] - ) + myCard: Option[String], + alreadyVoted: List[Player] + ) - /** view state for round after opening the votes - */ + /** view state for round after opening the votes + */ case Viewing( - votes: List[(PlayerID, String)] - ) + votes: List[(PlayerID, String)] + ) final case class PlayerID(id: Long) derives Codec.AsObject final case class Player(name: String, id: PlayerID) derives Codec.AsObject - final case class RoomID(id: Long) derives Codec.AsObject + final case class RoomID(name: String) derives Codec.AsObject + + final case class Room( + id: RoomID, + players: List[Player], + owner: PlayerID, + password: String, + allowedCards: List[String], + round: RoundState + ) { + def toViewFor(playerId: PlayerID): RoomStateView = { + players + .find(_.id == playerId) + .fold(ifEmpty = RoomStateView.empty)((me: Player) => + RoomStateView( + players, + me.id, + allowedCards, + round, + playerId == owner + ) + ) + } + } + object Room { + val testRoom = Room( + id = RoomID("testroom"), + players = List( + Player("me", PlayerID(1L)), + Player("horsey", PlayerID(444L)), + Player("froggy", PlayerID(555L)), + Player("owley", PlayerID(777L)) + ), + owner = PlayerID(1L), + password = "password", + allowedCards = List("S", "M", "L"), + // TODO - this needs to be a different hting + round = RoundState.Voting(None, List.empty) + ) + } }