backend - adding room service and wiring

preparing to create a better auth module that uses room service
This commit is contained in:
efim 2023-04-25 09:55:45 +04:00
parent dfff814079
commit 64267a5f67
5 changed files with 148 additions and 39 deletions

View File

@ -6,7 +6,8 @@ import cats.data.OptionT
import org.http4s.Request import org.http4s.Request
import industries.sunshine.planningpoker.common.Models.PlayerID import industries.sunshine.planningpoker.common.Models.PlayerID
import java.util.UUID 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 { trait Auth {
@ -24,7 +25,7 @@ trait Auth {
roomName: String, roomName: String,
roomPassword: String, roomPassword: String,
nickName: String nickName: String
): IO[Either[Unit, Long]] ): IO[Either[Unit, ResponseCookie]]
def leaveRoom( def leaveRoom(
sessionId: Long sessionId: Long
@ -36,38 +37,56 @@ object Auth {
def pureBadStub = new Auth { def pureBadStub = new Auth {
override def authUser = override def authUser =
Kleisli((req: Request[IO]) => 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( override def accessRoom(
roomName: String, roomName: String,
roomPassword: String, roomPassword: String,
nickName: 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] = override def leaveRoom(sessionId: Long): IO[Unit] =
IO(s"got request to leave for $sessionId") IO(s"got request to leave for $sessionId")
} }
type SessionsMap = Map[Long, (RoomID, PlayerID)] type SessionsMap = Map[Long, (RoomID, PlayerID)]
val sessionsRef = Ref.of[IO, SessionsMap](Map.empty) val sessionsRef =
val roomPasswordsRef = Ref.of[IO, Map[Long, String]](Map.empty) 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] = def apply(): IO[Auth] =
for { for {
store <- sessionsRef store <- sessionsRef
roomPasswords <- roomPasswordsRef rooms <- roomsRef
} yield new Auth { } yield new Auth {
val authcookie = "authcookie"
override def authUser = Kleisli { (req: Request[IO]) => 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( override def accessRoom(
roomName: String, roomName: String,
roomPassword: String, roomPassword: String,
nickName: String nickName: String
): IO[Either[Unit, Long]] = ??? ): IO[Either[Unit, ResponseCookie]] = {
???
}
override def leaveRoom(sessionId: Long): IO[Unit] = ??? override def leaveRoom(sessionId: Long): IO[Unit] = ???
} }

View File

@ -12,17 +12,21 @@ object BackendApp extends IOApp {
val host = host"0.0.0.0" val host = host"0.0.0.0"
val port = port"8080" val port = port"8080"
val server = for { val wiring = for {
srv <- EmberServerBuilder roomService <- Resource.eval(RoomService.make[IO])
httpService = MyHttpService.create(Auth.pureBadStub)
server <- EmberServerBuilder
.default[IO] .default[IO]
.withHost(host) .withHost(host)
.withPort(port) .withPort(port)
.withHttpWebSocketApp(MyHttpService.create(Auth.pureBadStub)(_)) .withHttpWebSocketApp(httpService(_))
.build .build
} yield srv } yield server
server.use(server => wiring.use(server =>
IO.delay(println(s"Server Has Started at ${server.address}")) >> IO.never.as(ExitCode.Success)) IO.delay(println(s"Server Has Started at ${server.address}")) >> IO.never
.as(ExitCode.Success)
)
} }
} }

View File

@ -62,12 +62,9 @@ object MyHttpService {
resp <- authResult match { resp <- authResult match {
case Left(error) => case Left(error) =>
Forbidden(error) Forbidden(error)
case Right(sessionId) => { case Right(authCookie) => {
Ok("Logged in!").map( IO(println(s"> logging in ${data.nickname} to ${data.roomName}")) >>
_.addCookie( Ok().map(_.addCookie(authCookie))
ResponseCookie("authcookie", sessionId.toString())
)
)
} }
} }
} yield resp } yield resp

View File

@ -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](_))
}
}

View File

@ -6,10 +6,14 @@ import io.circe._
object Models { object Models {
/** view of the single planning poker round /** view of the single planning poker round
* @param players - people who are currently playing * @param players
* @param allowedCards- the cards values that can be used by players * \- people who are currently playing
* @param round - state of the selected cards of the players * @param allowedCards-
* @param canCloseRound - whether current player has access to button to finish the round * 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( final case class RoomStateView(
players: List[Player], players: List[Player],
@ -23,15 +27,18 @@ object Models {
object RoomStateView { object RoomStateView {
val empty = 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: enum RoundState derives Codec.AsObject:
/** view state for round before votes are open player can know their vote and /** view state for round before votes are open player can know their vote
* who of the other players have voted * and who of the other players have voted
*/ */
case Voting( case Voting(
myCard: Option[String], myCard: Option[String],
@ -47,6 +54,45 @@ object Models {
final case class Player(name: String, id: PlayerID) 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)
)
}
} }