Compare commits

..

No commits in common. "90e886c62db6ad5a1f1063c3903ba3594a68fbdd" and "b29d1a1ef1d8d158c66346bc5976c9994af351f6" have entirely different histories.

9 changed files with 65 additions and 165 deletions

View File

@ -39,6 +39,31 @@ trait Auth[F[_]] {
} }
object Auth { 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)] type SessionsMap = Map[Long, (RoomID, PlayerID)]
class SimpleAuth[F[_]: Sync]( class SimpleAuth[F[_]: Sync](
@ -46,7 +71,7 @@ object Auth {
roomService: RoomService[F] roomService: RoomService[F]
) extends Auth[F] { ) extends Auth[F] {
val authcookieName = "authcookie" val authcookie = "authcookie"
override def joinRoom( override def joinRoom(
roomName: String, roomName: String,
@ -62,38 +87,29 @@ object Auth {
roomService.joinRoom(roomId, nickName, nickPassword, roomPassword) roomService.joinRoom(roomId, nickName, nickPassword, roomPassword)
) )
.leftMap(_.toString()) .leftMap(_.toString())
// newSessionId = Random.nextLong() // TODO return after i stop mocking RoomService newSessionId = Random.nextLong()
newSessionId = TestModels.testSessionId
_ <- EitherT.liftF(sessions.update(_.updated(newSessionId, (roomId, playerId)))) _ <- EitherT.liftF(sessions.update(_.updated(newSessionId, (roomId, playerId))))
} yield ResponseCookie( } yield ResponseCookie(name = authcookie, content = newSessionId.toString(), secure = true)
name = authcookieName,
content = newSessionId.toString(),
secure = true
)
result.value result.value
} }
override def authUser override def authUser
: Kleisli[[A] =>> cats.data.OptionT[F, A], Request[F], (PlayerID, RoomID)] = { : Kleisli[[A] =>> cats.data.OptionT[F, A], Request[F], (PlayerID, RoomID)] = {
Kleisli { (request: Request[F]) => // check authcookie presence, exchange it for playerID ad roomID
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] = { override def deleteSession(sessionId: Long): F[Unit] = {
sessions.update(_.removed(sessionId)) // i suppose leaving the room should just be authed route & method
???
} }
} }
def make[F[_]: Sync](roomsService: RoomService[F]): F[Auth[F]] = def make[F[_]: Sync](roomsService: RoomService[F]): F[Auth[F]] =
for { for {
sessionsMap <- Ref.of[F, SessionsMap](TestModels.testSessions) sessionsMap <- Ref.of[F, SessionsMap](
Map(1L -> (RoomID("testroom"), PlayerID(1L)))
)
} yield new SimpleAuth(sessionsMap, roomsService) } yield new SimpleAuth(sessionsMap, roomsService)
} }

View File

@ -14,8 +14,7 @@ object BackendApp extends IOApp {
val wiring = for { val wiring = for {
roomService <- Resource.eval(RoomService.make[IO]) roomService <- Resource.eval(RoomService.make[IO])
auth <- Resource.eval(Auth.make(roomService)) httpService = MyHttpService.create(Auth.pureBadStub)
httpService = MyHttpService.create(auth)
server <- EmberServerBuilder server <- EmberServerBuilder
.default[IO] .default[IO]
.withHost(host) .withHost(host)

View File

@ -24,19 +24,17 @@ object MyHttpService {
AuthedRoutes.of { AuthedRoutes.of {
case GET -> Root / "subscribe" as (playerId, roomId) => { case GET -> Root / "subscribe" as (playerId, roomId) => {
val send: Stream[IO, WebSocketFrame] = val send: Stream[IO, WebSocketFrame] =
( Stream
Stream .emits(TestModels.testChangesList)
.emits(TestModels.testChangesList) .covary[IO]
.covary[IO] .metered(1.second)
.metered(1.second) ++ Stream.never[IO]
)
.map(state => WebSocketFrame.Text(state.asJson.noSpaces)) .map(state => WebSocketFrame.Text(state.asJson.noSpaces))
val receive: Pipe[IO, WebSocketFrame, Unit] = _.evalMap { val receive: Pipe[IO, WebSocketFrame, Unit] = _.evalMap {
case WebSocketFrame.Text(text, _) => Sync[IO].delay(println(text)) case WebSocketFrame.Text(text, _) => Sync[IO].delay(println(text))
case other => Sync[IO].delay(println(other)) case other => Sync[IO].delay(println(other))
} }
IO(println(s"got ws request from $playerId in $roomId")) >> wsb.build(send, receive) wsb.build(send, receive)
} }
case GET -> Root / "vote" / vote as (playerId, roomId) => { case GET -> Root / "vote" / vote as (playerId, roomId) => {
// TODO forward these to the service implementation // TODO forward these to the service implementation
@ -62,8 +60,7 @@ object MyHttpService {
data.roomName, data.roomName,
data.password, data.password,
data.nickname, data.nickname,
data.nickPassword data.nickPassword)
)
resp <- authResult match { resp <- authResult match {
case Left(error) => case Left(error) =>
Forbidden(error) Forbidden(error)

View File

@ -91,6 +91,6 @@ class InMemoryRoomService[F[_]: Sync](stateRef: Ref[F, Map[RoomID, Room]]) exten
} }
object RoomService { object RoomService {
def make[F[_]: Sync]: F[RoomService[F]] = { def make[F[_]: Sync]: F[RoomService[F]] = {
Ref.of[F, Map[RoomID, Room]](TestModels.testRooms).map(new InMemoryRoomService[F](_)) Ref.of[F, Map[RoomID, Room]](Map.empty).map(new InMemoryRoomService[F](_))
} }
} }

View File

@ -33,7 +33,7 @@ lazy val frontend = project
libraryDependencies += "org.scala-js" %%% "scalajs-dom" % "2.4.0", libraryDependencies += "org.scala-js" %%% "scalajs-dom" % "2.4.0",
libraryDependencies += "com.raquo" %%% "laminar" % "15.0.1", libraryDependencies += "com.raquo" %%% "laminar" % "15.0.1",
libraryDependencies += "io.laminext" %%% "websocket-circe" % "0.15.0", libraryDependencies += "io.laminext" %%% "websocket-circe" % "0.15.0",
libraryDependencies += "io.laminext" %%% "fetch-circe" % "0.15.0" libraryDependencies += "io.laminext" %%% "fetch" % "0.15.0"
) )
.dependsOn(common.js) .dependsOn(common.js)

View File

@ -63,7 +63,7 @@ object Models {
final case class Room( final case class Room(
id: RoomID, id: RoomID,
players: List[Player], players: List[Player],
owner: PlayerID, // TODO switch to nickname owner: PlayerID,
password: String, password: String,
allowedCards: List[String], allowedCards: List[String],
round: RoundState, round: RoundState,

View File

@ -3,26 +3,31 @@ package industries.sunshine.planningpoker
import industries.sunshine.planningpoker.common.Models.* import industries.sunshine.planningpoker.common.Models.*
object TestModels { object TestModels {
val me = Player("me", PlayerID(1)) val me = Player("wormy", PlayerID(1))
val pony = Player("pony", PlayerID(10)) val pony = Player("pony", PlayerID(10))
val birdy = Player("birdy", PlayerID(11)) val birdy = Player("birdy", PlayerID(11))
val horsey = Player("horsey", PlayerID(12)) val horsey = Player("horsey", PlayerID(12))
val testRoomBackend = Room( val testRoom = {
id = RoomID("testroom"), RoomStateView(
players = List(me, birdy, pony, horsey), players = List(me, birdy, pony),
owner = me.id, me = me.id,
password = "password",
allowedCards = List("xs", "s", "m", "l", "xl"), allowedCards = List("xs", "s", "m", "l", "xl"),
round = RoundState.Viewing( round = RoundState.Voting(myCard = Some("s"), alreadyVoted = List(me, pony)),
canCloseRound = true
)
}
val testOpenedRoom = {
RoomStateView(
players = List(me, birdy, pony, horsey),
me = me.id,
allowedCards = List("xs", "s", "m", "l", "xl"),
round = RoundState.Viewing(
List(me.id -> "xs", pony.id -> "l", birdy.id -> "s", horsey.id -> "m") List(me.id -> "xs", pony.id -> "l", birdy.id -> "s", horsey.id -> "m")
), ),
playersPasswords = Map("me" -> "nickpassword") // nickname into password canCloseRound = true
) )
}
val testSessionId = 1L
val testSessions = Map(testSessionId -> (testRoomBackend.id, me.id))
val testRooms = Map(testRoomBackend.id -> testRoomBackend)
val testChangesList = List( val testChangesList = List(
RoomStateView( RoomStateView(

View File

@ -25,6 +25,7 @@ object Main {
) )
// TODO is this ok for state creation? link with auth component and use in another? // 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 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.WebSocket._
import io.laminext.websocket.circe.webSocketReceiveBuilderSyntax import io.laminext.websocket.circe.webSocketReceiveBuilderSyntax
@ -59,8 +60,7 @@ object Main {
className := "h-24 w-full flex flex-for justify-center items-center bg-green-200", className := "h-24 w-full flex flex-for justify-center items-center bg-green-200",
p(className := "text-2xl", "Here be header") p(className := "text-2xl", "Here be header")
), ),
child <-- roomStateWSStream.isConnected.map( if (_) emptyNode else JoinRoomComponent.render()), RoomView.renderRoom(stateSignal),
child <-- roomStateWSStream.isConnected.map( if (_) RoomView.renderRoom(stateSignal) else emptyNode),
roomStateWSStream.connect roomStateWSStream.connect
) )
} }

View File

@ -1,117 +0,0 @@
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)
)
)
)
}
)
)
)
}
}