Compare commits
	
		
			3 Commits
		
	
	
		
			b29d1a1ef1
			...
			90e886c62d
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					90e886c62d | ||
| 
						 | 
					1f28a03d47 | ||
| 
						 | 
					6c1220b544 | 
@ -39,31 +39,6 @@ trait Auth[F[_]] {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
object Auth {
 | 
			
		||||
  def pureBadStub = new Auth[IO] {
 | 
			
		||||
    override def authUser =
 | 
			
		||||
      Kleisli((req: Request[IO]) =>
 | 
			
		||||
        OptionT.liftF(
 | 
			
		||||
          IO(println(s"authUser: $req")) >> IO(
 | 
			
		||||
            PlayerID(14) -> RoomID("testroom")
 | 
			
		||||
          )
 | 
			
		||||
        )
 | 
			
		||||
      )
 | 
			
		||||
    override def joinRoom(
 | 
			
		||||
        roomName: String,
 | 
			
		||||
        roomPassword: String,
 | 
			
		||||
        nickName: String,
 | 
			
		||||
        nickPassword: String
 | 
			
		||||
    ) =
 | 
			
		||||
      IO(
 | 
			
		||||
        println(
 | 
			
		||||
          s"> access room for $roomName $roomPassword $nickName, to return stub 111"
 | 
			
		||||
        )
 | 
			
		||||
      ) >> IO.pure(Right(ResponseCookie("authcookie", "1")))
 | 
			
		||||
 | 
			
		||||
    override def deleteSession(sessionId: Long): IO[Unit] =
 | 
			
		||||
      IO(s"got request to leave for $sessionId")
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  type SessionsMap = Map[Long, (RoomID, PlayerID)]
 | 
			
		||||
 | 
			
		||||
  class SimpleAuth[F[_]: Sync](
 | 
			
		||||
@ -71,7 +46,7 @@ object Auth {
 | 
			
		||||
      roomService: RoomService[F]
 | 
			
		||||
  ) extends Auth[F] {
 | 
			
		||||
 | 
			
		||||
    val authcookie = "authcookie"
 | 
			
		||||
    val authcookieName = "authcookie"
 | 
			
		||||
 | 
			
		||||
    override def joinRoom(
 | 
			
		||||
        roomName: String,
 | 
			
		||||
@ -87,29 +62,38 @@ object Auth {
 | 
			
		||||
          roomService.joinRoom(roomId, nickName, nickPassword, roomPassword)
 | 
			
		||||
        )
 | 
			
		||||
          .leftMap(_.toString())
 | 
			
		||||
        newSessionId = Random.nextLong()
 | 
			
		||||
        // newSessionId = Random.nextLong() // TODO return after i stop mocking RoomService
 | 
			
		||||
        newSessionId = TestModels.testSessionId
 | 
			
		||||
        _ <- EitherT.liftF(sessions.update(_.updated(newSessionId, (roomId, playerId))))
 | 
			
		||||
      } yield ResponseCookie(name = authcookie, content = newSessionId.toString(), secure = true)
 | 
			
		||||
      } yield ResponseCookie(
 | 
			
		||||
        name = authcookieName,
 | 
			
		||||
        content = newSessionId.toString(),
 | 
			
		||||
        secure = true
 | 
			
		||||
      )
 | 
			
		||||
 | 
			
		||||
      result.value
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override def authUser
 | 
			
		||||
        : Kleisli[[A] =>> cats.data.OptionT[F, A], Request[F], (PlayerID, RoomID)] = {
 | 
			
		||||
      // check authcookie presence, exchange it for playerID ad roomID
 | 
			
		||||
      ???
 | 
			
		||||
      Kleisli { (request: Request[F]) =>
 | 
			
		||||
        OptionT(sessions.get.map { sessionsMap =>
 | 
			
		||||
          for {
 | 
			
		||||
            authcookie <- request.cookies.find(_.name == authcookieName)
 | 
			
		||||
            sessionId <- authcookie.content.toLongOption
 | 
			
		||||
            (roomId, playerId) <- sessionsMap.get(sessionId)
 | 
			
		||||
          } yield (playerId, roomId)
 | 
			
		||||
        })
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override def deleteSession(sessionId: Long): F[Unit] = {
 | 
			
		||||
      // i suppose leaving the room should just be authed route & method
 | 
			
		||||
      ???
 | 
			
		||||
      sessions.update(_.removed(sessionId))
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  def make[F[_]: Sync](roomsService: RoomService[F]): F[Auth[F]] =
 | 
			
		||||
    for {
 | 
			
		||||
      sessionsMap <- Ref.of[F, SessionsMap](
 | 
			
		||||
        Map(1L -> (RoomID("testroom"), PlayerID(1L)))
 | 
			
		||||
      )
 | 
			
		||||
      sessionsMap <- Ref.of[F, SessionsMap](TestModels.testSessions)
 | 
			
		||||
    } yield new SimpleAuth(sessionsMap, roomsService)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -14,7 +14,8 @@ object BackendApp extends IOApp {
 | 
			
		||||
 | 
			
		||||
    val wiring = for {
 | 
			
		||||
      roomService <- Resource.eval(RoomService.make[IO])
 | 
			
		||||
      httpService = MyHttpService.create(Auth.pureBadStub)
 | 
			
		||||
      auth <- Resource.eval(Auth.make(roomService))
 | 
			
		||||
      httpService = MyHttpService.create(auth)
 | 
			
		||||
      server <- EmberServerBuilder
 | 
			
		||||
        .default[IO]
 | 
			
		||||
        .withHost(host)
 | 
			
		||||
 | 
			
		||||
@ -24,17 +24,19 @@ object MyHttpService {
 | 
			
		||||
      AuthedRoutes.of {
 | 
			
		||||
        case GET -> Root / "subscribe" as (playerId, roomId) => {
 | 
			
		||||
          val send: Stream[IO, WebSocketFrame] =
 | 
			
		||||
            Stream
 | 
			
		||||
              .emits(TestModels.testChangesList)
 | 
			
		||||
              .covary[IO]
 | 
			
		||||
              .metered(1.second)
 | 
			
		||||
            (
 | 
			
		||||
              Stream
 | 
			
		||||
                .emits(TestModels.testChangesList)
 | 
			
		||||
                .covary[IO]
 | 
			
		||||
                .metered(1.second) ++ Stream.never[IO]
 | 
			
		||||
            )
 | 
			
		||||
              .map(state => WebSocketFrame.Text(state.asJson.noSpaces))
 | 
			
		||||
 | 
			
		||||
          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)
 | 
			
		||||
          IO(println(s"got ws request from $playerId in $roomId")) >> wsb.build(send, receive)
 | 
			
		||||
        }
 | 
			
		||||
        case GET -> Root / "vote" / vote as (playerId, roomId) => {
 | 
			
		||||
          // TODO forward these to the service implementation
 | 
			
		||||
@ -60,7 +62,8 @@ object MyHttpService {
 | 
			
		||||
              data.roomName,
 | 
			
		||||
              data.password,
 | 
			
		||||
              data.nickname,
 | 
			
		||||
              data.nickPassword)
 | 
			
		||||
              data.nickPassword
 | 
			
		||||
            )
 | 
			
		||||
            resp <- authResult match {
 | 
			
		||||
              case Left(error) =>
 | 
			
		||||
                Forbidden(error)
 | 
			
		||||
 | 
			
		||||
@ -91,6 +91,6 @@ class InMemoryRoomService[F[_]: Sync](stateRef: Ref[F, Map[RoomID, Room]]) exten
 | 
			
		||||
}
 | 
			
		||||
object RoomService {
 | 
			
		||||
  def make[F[_]: Sync]: F[RoomService[F]] = {
 | 
			
		||||
    Ref.of[F, Map[RoomID, Room]](Map.empty).map(new InMemoryRoomService[F](_))
 | 
			
		||||
    Ref.of[F, Map[RoomID, Room]](TestModels.testRooms).map(new InMemoryRoomService[F](_))
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -33,7 +33,7 @@ lazy val frontend = project
 | 
			
		||||
    libraryDependencies += "org.scala-js" %%% "scalajs-dom" % "2.4.0",
 | 
			
		||||
    libraryDependencies += "com.raquo" %%% "laminar" % "15.0.1",
 | 
			
		||||
    libraryDependencies += "io.laminext" %%% "websocket-circe" % "0.15.0",
 | 
			
		||||
    libraryDependencies += "io.laminext" %%% "fetch" % "0.15.0"
 | 
			
		||||
    libraryDependencies += "io.laminext" %%% "fetch-circe" % "0.15.0"
 | 
			
		||||
  )
 | 
			
		||||
  .dependsOn(common.js)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -63,7 +63,7 @@ object Models {
 | 
			
		||||
  final case class Room(
 | 
			
		||||
      id: RoomID,
 | 
			
		||||
      players: List[Player],
 | 
			
		||||
      owner: PlayerID,
 | 
			
		||||
      owner: PlayerID, // TODO switch to nickname
 | 
			
		||||
      password: String,
 | 
			
		||||
      allowedCards: List[String],
 | 
			
		||||
      round: RoundState,
 | 
			
		||||
 | 
			
		||||
@ -3,31 +3,26 @@ package industries.sunshine.planningpoker
 | 
			
		||||
import industries.sunshine.planningpoker.common.Models.*
 | 
			
		||||
 | 
			
		||||
object TestModels {
 | 
			
		||||
  val me = Player("wormy", PlayerID(1))
 | 
			
		||||
  val me = Player("me", PlayerID(1))
 | 
			
		||||
  val pony = Player("pony", PlayerID(10))
 | 
			
		||||
  val birdy = Player("birdy", PlayerID(11))
 | 
			
		||||
  val horsey = Player("horsey", PlayerID(12))
 | 
			
		||||
 | 
			
		||||
  val testRoom = {
 | 
			
		||||
    RoomStateView(
 | 
			
		||||
      players = List(me, birdy, pony),
 | 
			
		||||
      me = me.id,
 | 
			
		||||
      allowedCards = List("xs", "s", "m", "l", "xl"),
 | 
			
		||||
      round = RoundState.Voting(myCard = Some("s"), alreadyVoted = List(me, pony)),
 | 
			
		||||
      canCloseRound = true
 | 
			
		||||
    )
 | 
			
		||||
  }
 | 
			
		||||
  val testOpenedRoom = {
 | 
			
		||||
    RoomStateView(
 | 
			
		||||
  val testRoomBackend = Room(
 | 
			
		||||
      id = RoomID("testroom"),
 | 
			
		||||
      players = List(me, birdy, pony, horsey),
 | 
			
		||||
      me = me.id,
 | 
			
		||||
      owner = me.id,
 | 
			
		||||
      password = "password",
 | 
			
		||||
      allowedCards = List("xs", "s", "m", "l", "xl"),
 | 
			
		||||
      round = RoundState.Viewing(
 | 
			
		||||
            round = RoundState.Viewing(
 | 
			
		||||
        List(me.id -> "xs", pony.id -> "l", birdy.id -> "s", horsey.id -> "m")
 | 
			
		||||
      ),
 | 
			
		||||
      canCloseRound = true
 | 
			
		||||
    )
 | 
			
		||||
  }
 | 
			
		||||
      playersPasswords = Map("me" -> "nickpassword") // nickname into password
 | 
			
		||||
  )
 | 
			
		||||
 | 
			
		||||
  val testSessionId = 1L
 | 
			
		||||
  val testSessions = Map(testSessionId -> (testRoomBackend.id, me.id))
 | 
			
		||||
  val testRooms = Map(testRoomBackend.id -> testRoomBackend)
 | 
			
		||||
 | 
			
		||||
  val testChangesList = List(
 | 
			
		||||
    RoomStateView(
 | 
			
		||||
 | 
			
		||||
@ -25,7 +25,6 @@ object Main {
 | 
			
		||||
  )
 | 
			
		||||
  // TODO is this ok for state creation? link with auth component and use in another?
 | 
			
		||||
  val appStateSignal = Var(AppState(Some(TestModels.me.id))).signal
 | 
			
		||||
  val staticStateSignal = Var(TestModels.testRoom).signal
 | 
			
		||||
 | 
			
		||||
  import io.laminext.websocket.circe.WebSocket._
 | 
			
		||||
  import io.laminext.websocket.circe.webSocketReceiveBuilderSyntax
 | 
			
		||||
@ -60,7 +59,8 @@ object Main {
 | 
			
		||||
        className := "h-24 w-full flex flex-for justify-center items-center bg-green-200",
 | 
			
		||||
        p(className := "text-2xl", "Here be header")
 | 
			
		||||
      ),
 | 
			
		||||
      RoomView.renderRoom(stateSignal),
 | 
			
		||||
      child <-- roomStateWSStream.isConnected.map( if (_) emptyNode else  JoinRoomComponent.render()),
 | 
			
		||||
      child <-- roomStateWSStream.isConnected.map( if (_) RoomView.renderRoom(stateSignal) else  emptyNode),
 | 
			
		||||
      roomStateWSStream.connect
 | 
			
		||||
    )
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@ -0,0 +1,117 @@
 | 
			
		||||
package industries.sunshine.planningpoker
 | 
			
		||||
 | 
			
		||||
import scala.scalajs.js
 | 
			
		||||
import scala.scalajs.js.annotation.*
 | 
			
		||||
import com.raquo.laminar.api.L.{*, given}
 | 
			
		||||
import io.laminext.fetch.Fetch
 | 
			
		||||
import io.laminext.fetch.circe._
 | 
			
		||||
 | 
			
		||||
import scala.util.Failure
 | 
			
		||||
import scala.util.Success
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
import concurrent.ExecutionContext.Implicits.global
 | 
			
		||||
 | 
			
		||||
object JoinRoomComponent {
 | 
			
		||||
  // TODO inputs for room name, room password, nick name, nick password
 | 
			
		||||
  // do the get to /login route
 | 
			
		||||
  // display errors if any.
 | 
			
		||||
  // but then what? attempt to start the websocket?
 | 
			
		||||
  // and if websocket closed show this component,
 | 
			
		||||
  // if it's open - doesn't show this component, show the room.
 | 
			
		||||
  // i suppose it should be managed on a level up
 | 
			
		||||
  // or rather what? ws stream should be retried every time someone presses submit button
 | 
			
		||||
  // and receives 200 ok
 | 
			
		||||
  // so, parent page should send in observer for the successful auth. and on that observer - start \ restart the websocket
 | 
			
		||||
  def nameInput(data: Var[String], placeholderText: String) = input(
 | 
			
		||||
    className := "border-2 m-1 rounded",
 | 
			
		||||
    placeholder := placeholderText,
 | 
			
		||||
    controlled(
 | 
			
		||||
      value <-- data.signal,
 | 
			
		||||
      onInput.mapToValue --> data.writer
 | 
			
		||||
    )
 | 
			
		||||
  )
 | 
			
		||||
  def passInput(data: Var[String], placeholderText: String) = input(
 | 
			
		||||
    tpe := "password",
 | 
			
		||||
    className := "border-2 m-1 rounded",
 | 
			
		||||
    placeholder := placeholderText,
 | 
			
		||||
    controlled(
 | 
			
		||||
      value <-- data.signal,
 | 
			
		||||
      onInput.mapToValue --> data.writer
 | 
			
		||||
    )
 | 
			
		||||
  )
 | 
			
		||||
 | 
			
		||||
  val roomNameVar = Var("testroom")
 | 
			
		||||
  val roomPassVar = Var("password")
 | 
			
		||||
  val nicknameVar = Var("me")
 | 
			
		||||
  val nicknamePass = Var("nickpass")
 | 
			
		||||
 | 
			
		||||
  val (responsesStream, responseReceived) = EventStream.withCallback[FetchResponse[String]]
 | 
			
		||||
 | 
			
		||||
  val submitButton = button(
 | 
			
		||||
    "Join room",
 | 
			
		||||
    onClick
 | 
			
		||||
      .mapTo {
 | 
			
		||||
        (roomNameVar.now(), roomPassVar.now(), nicknameVar.now(), nicknamePass.now())
 | 
			
		||||
      }
 | 
			
		||||
      .flatMap { case (roomName, roomPass, nickname, nicknamePass) =>
 | 
			
		||||
        Fetch
 | 
			
		||||
          .post(
 | 
			
		||||
            "/api/login",
 | 
			
		||||
            body = Requests.LogIn(
 | 
			
		||||
              roomName,
 | 
			
		||||
              nickname,
 | 
			
		||||
              roomPass,
 | 
			
		||||
              nicknamePass
 | 
			
		||||
            )
 | 
			
		||||
          )
 | 
			
		||||
          .text
 | 
			
		||||
      } --> responseReceived
 | 
			
		||||
  )
 | 
			
		||||
 | 
			
		||||
  def render(): Element = {
 | 
			
		||||
    div(
 | 
			
		||||
      className := "flex flex-col h-full justify-center",
 | 
			
		||||
      "Logging in:",
 | 
			
		||||
      nameInput(roomNameVar, "Enter room name:"),
 | 
			
		||||
      passInput(roomPassVar, "room password"),
 | 
			
		||||
      nameInput(nicknameVar, "Enter your nickname:"),
 | 
			
		||||
      passInput(nicknamePass, "nickname pass:"),
 | 
			
		||||
      submitButton,
 | 
			
		||||
      div(
 | 
			
		||||
        div(
 | 
			
		||||
          code("received:")
 | 
			
		||||
        ),
 | 
			
		||||
        div(
 | 
			
		||||
          cls := "flex flex-col space-y-4 p-4 max-h-96 overflow-auto bg-gray-900",
 | 
			
		||||
          children.command <-- responsesStream.recoverToTry.map {
 | 
			
		||||
            case Success(response) =>
 | 
			
		||||
              CollectionCommand.Append(
 | 
			
		||||
                div(
 | 
			
		||||
                  div(
 | 
			
		||||
                    cls := "flex space-x-2 items-center",
 | 
			
		||||
                    code(cls := "text-green-500", "Status: "),
 | 
			
		||||
                    code(cls := "text-green-300", s"${response.status} ${response.statusText}")
 | 
			
		||||
                  ),
 | 
			
		||||
                  div(
 | 
			
		||||
                    cls := "text-green-400 text-xs",
 | 
			
		||||
                    code(response.data)
 | 
			
		||||
                  )
 | 
			
		||||
                )
 | 
			
		||||
              )
 | 
			
		||||
            case Failure(exception) =>
 | 
			
		||||
              CollectionCommand.Append(
 | 
			
		||||
                div(
 | 
			
		||||
                  div(
 | 
			
		||||
                    cls := "flex space-x-2 items-center",
 | 
			
		||||
                    code(cls := "text-red-500", "Error: "),
 | 
			
		||||
                    code(cls := "text-red-300", exception.getMessage)
 | 
			
		||||
                  )
 | 
			
		||||
                )
 | 
			
		||||
              )
 | 
			
		||||
          }
 | 
			
		||||
        )
 | 
			
		||||
      )
 | 
			
		||||
    )
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user