From c6bfdacd1d8e1b97772a3d481f55a6ab636a3bbf Mon Sep 17 00:00:00 2001 From: efim Date: Sun, 23 Apr 2023 13:40:37 +0400 Subject: [PATCH] dummy Auth and authed routes --- .../sunshine/planningpoker/Auth.scala | 74 +++++++++++ .../sunshine/planningpoker/BackendApp.scala | 52 ++------ .../planningpoker/MyHttpService.scala | 79 ++++++++++++ .../sunshine/planningpoker/Models.scala | 121 +++++++++--------- 4 files changed, 229 insertions(+), 97 deletions(-) create mode 100644 backend/src/main/scala/industries/sunshine/planningpoker/Auth.scala create mode 100644 backend/src/main/scala/industries/sunshine/planningpoker/MyHttpService.scala diff --git a/backend/src/main/scala/industries/sunshine/planningpoker/Auth.scala b/backend/src/main/scala/industries/sunshine/planningpoker/Auth.scala new file mode 100644 index 0000000..a579426 --- /dev/null +++ b/backend/src/main/scala/industries/sunshine/planningpoker/Auth.scala @@ -0,0 +1,74 @@ +package industries.sunshine.planningpoker + +import cats.effect._ +import cats.data.Kleisli +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 + +trait Auth { + + /** for middleware that converts sessionId into PlayerId + */ + def authUser: Kleisli[[A] =>> cats.data.OptionT[cats.effect.IO, A], Request[ + cats.effect.IO + ], (PlayerID, RoomID)] + + /** Get sessionId for accessing a room + * @param roomPassword + * \- requirement to get access to the room + */ + def accessRoom( + roomName: String, + roomPassword: String, + nickName: String + ): IO[Either[Unit, Long]] + + def leaveRoom( + sessionId: Long + ): IO[Unit] + +} + +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))) + ) + 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)) + + 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) + + def apply(): IO[Auth] = + for { + store <- sessionsRef + roomPasswords <- roomPasswordsRef + } yield new Auth { + override def authUser = Kleisli { (req: Request[IO]) => + { + ??? + } + } + override def accessRoom( + roomName: String, + roomPassword: String, + nickName: String + ): IO[Either[Unit, Long]] = ??? + + 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 e6e6d73..7bf6596 100644 --- a/backend/src/main/scala/industries/sunshine/planningpoker/BackendApp.scala +++ b/backend/src/main/scala/industries/sunshine/planningpoker/BackendApp.scala @@ -2,53 +2,27 @@ package industries.sunshine.planningpoker import cats.effect._ import cats.syntax.all._ -import org.http4s._, org.http4s.dsl.io._, org.http4s.implicits._ import com.comcast.ip4s._ -import org.http4s.HttpRoutes -import org.http4s.dsl.io._ -import org.http4s.implicits._ import org.http4s.ember.server._ -import org.http4s.websocket.WebSocketFrame - -import io.circe.generic.auto._ -import org.http4s.circe.CirceEntityDecoder._ -import scala.concurrent.duration._ -import org.http4s.server.websocket.WebSocketBuilder import fs2._ object BackendApp extends IOApp.Simple { - def service(wsb: WebSocketBuilder[IO]) = HttpRoutes - .of[IO] { - case req @ POST -> Root / login => { - for { - data <- req.as[Requests.LogIn] - resp <- Ok(s"got some: ${data}") - } yield resp - } - case GET -> Root / subscribe => { - val send: Stream[IO, WebSocketFrame] = - Stream.awakeEvery[IO](1.seconds).map(_ => WebSocketFrame.Text("text")) - val receive: Pipe[IO, WebSocketFrame, Unit] = _.evalMap { - case WebSocketFrame.Text(text, _) => Sync[IO].delay(println(text)) - case other => Sync[IO].delay(println(other)) - } - wsb.build(send, receive) - } - case _ => Ok("hello") - } - .orNotFound - override def run: IO[Unit] = { - val a = 1 - IO.println(s"Hello, World! $a") >> - EmberServerBuilder + val host = host"0.0.0.0" + val port = port"8080" + + val server = for { + srv <- EmberServerBuilder .default[IO] - .withHost(ipv4"0.0.0.0") - .withPort(port"8080") - .withHttpWebSocketApp(service) + .withHost(host) + .withPort(port) + .withHttpWebSocketApp(MyHttpService.create(Auth.pureBadStub)(_)) .build - .use(_ => IO.never) - .as(ExitCode.Success) + } yield srv + + server.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 new file mode 100644 index 0000000..20b783b --- /dev/null +++ b/backend/src/main/scala/industries/sunshine/planningpoker/MyHttpService.scala @@ -0,0 +1,79 @@ +package industries.sunshine.planningpoker + +import cats.effect._ +import cats.syntax.all._ +import org.http4s._, org.http4s.dsl.io._, org.http4s.implicits._ +import org.http4s.websocket.WebSocketFrame +import io.circe.generic.auto._ +import org.http4s.circe.CirceEntityDecoder._ +import scala.concurrent.duration._ +import org.http4s.server.websocket.WebSocketBuilder +import fs2._ +import industries.sunshine.planningpoker.common.Models.RoomID +import industries.sunshine.planningpoker.common.Models.PlayerID +import org.http4s.server.AuthMiddleware.apply +import org.http4s.server.AuthMiddleware + +object MyHttpService { + def create(auth: Auth)( + wsb: WebSocketBuilder[cats.effect.IO] + ): HttpApp[cats.effect.IO] = { + + val authedRoomRoutes: AuthedRoutes[(PlayerID, RoomID), IO] = + AuthedRoutes.of { + case GET -> Root / subscribe as (playerId, roomId) => { + val send: Stream[IO, WebSocketFrame] = + Stream + .awakeEvery[IO](1.seconds) + .map(_ => WebSocketFrame.Text("text")) + val receive: Pipe[IO, WebSocketFrame, Unit] = _.evalMap { + case WebSocketFrame.Text(text, _) => Sync[IO].delay(println(text)) + case other => Sync[IO].delay(println(other)) + } + wsb.build(send, receive) + } + case GET -> Root / "vote" / vote as (playerId, roomId) => { + // TODO forward these to the service implementation + Ok(s">> got $vote from $playerId in $roomId") + } + case GET -> Root / "end-voting" as (playerId, roomId) => { + Ok(s">> got request to end voting from $playerId in $roomId") + } + case GET -> Root / "new-poll" as (playerId, roomId) => { + Ok(s">> got request to start new voting from $playerId in $roomId") + } + } + + val authMiddleware = AuthMiddleware(auth.authUser) + val aa = authMiddleware(authedRoomRoutes) + + val authenticationRoute = HttpRoutes + .of[IO] { + case req @ POST -> Root / login => { + for { + data <- req.as[Requests.LogIn] + authResult <- auth.accessRoom( + data.roomName, + data.password, + data.nickname + ) + resp <- authResult match { + case Left(error) => + Forbidden(error) + case Right(sessionId) => { + Ok("Logged in!").map( + _.addCookie( + ResponseCookie("authcookie", sessionId.toString()) + ) + ) + } + } + } yield resp + } + case _ => Ok("hello") + } + + (authenticationRoute <+> authMiddleware(authedRoomRoutes)).orNotFound + } + +} diff --git a/common/src/main/scala/industries/sunshine/planningpoker/Models.scala b/common/src/main/scala/industries/sunshine/planningpoker/Models.scala index 95d5f20..1d559ce 100644 --- a/common/src/main/scala/industries/sunshine/planningpoker/Models.scala +++ b/common/src/main/scala/industries/sunshine/planningpoker/Models.scala @@ -3,70 +3,75 @@ package industries.sunshine.planningpoker.common import java.util.UUID 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 - */ -final case class RoomStateView( - players: List[Player], - me: PlayerID, - allowedCards: List[String], - round: RoundState, - canCloseRound: Boolean = false -) { - def playersCount: Int = players.size -} -object RoomStateView { - val me = Player("wormy", PlayerID()) - val testRoom = { - val pony = Player("pony", PlayerID()) - RoomStateView( - players = List(me, Player("birdy", PlayerID()), pony), - me = me.id, - allowedCards = List("xs", "s", "m", "l", "xl"), - round = VotingRound(myCard = Some("s"), alreadyVoted = List(me, pony)), - canCloseRound = true - ) - } - val testOpenedRoom = { - val pony = Player("pony", PlayerID()) - val birdy = Player("birdy", PlayerID()) - val horsey = Player("horsey", PlayerID()) - RoomStateView( - players = List(me, birdy, pony, horsey), - me = me.id, - allowedCards = List("xs", "s", "m", "l", "xl"), - round = ViewingRound(Map( me.id -> "xs", pony.id -> "l", birdy.id -> "s", horsey.id -> "m" )), - canCloseRound = true - ) + /** 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 + */ + final case class RoomStateView( + players: List[Player], + me: PlayerID, + allowedCards: List[String], + round: RoundState, + canCloseRound: Boolean = false + ) { + def playersCount: Int = players.size } -} + object RoomStateView { + val me = Player("wormy", PlayerID(1)) + val testRoom = { + val pony = Player("pony", PlayerID(2)) + RoomStateView( + players = List(me, Player("birdy", PlayerID(3)), pony), + me = me.id, + allowedCards = List("xs", "s", "m", "l", "xl"), + round = VotingRound(myCard = Some("s"), alreadyVoted = List(me, pony)), + canCloseRound = true + ) + } + val testOpenedRoom = { + val pony = Player("pony", PlayerID(10)) + val birdy = Player("birdy", PlayerID(11)) + val horsey = Player("horsey", PlayerID(12)) + RoomStateView( + players = List(me, birdy, pony, horsey), + me = me.id, + allowedCards = List("xs", "s", "m", "l", "xl"), + round = ViewingRound( + Map(me.id -> "xs", pony.id -> "l", birdy.id -> "s", horsey.id -> "m") + ), + canCloseRound = true + ) + } -trait RoundState + } -/** view state for round before votes are open player can know their vote and - * who of the other players have voted - */ -final case class VotingRound( - myCard: Option[String], - alreadyVoted: List[Player] -) extends RoundState + trait RoundState -/** view state for round after opening the votes - */ -final case class ViewingRound( - votes: Map[PlayerID, String] -) extends RoundState + /** view state for round before votes are open player can know their vote and + * who of the other players have voted + */ + final case class VotingRound( + myCard: Option[String], + alreadyVoted: List[Player] + ) extends RoundState -final class PlayerID -final case class Player(name: String, id: PlayerID) + /** view state for round after opening the votes + */ + final case class ViewingRound( + votes: Map[PlayerID, String] + ) extends RoundState + + final case class PlayerID(id: Long) + final case class Player(name: String, id: PlayerID) + + final case class RoomID(id: Long) }