Compare commits
No commits in common. "90e886c62db6ad5a1f1063c3903ba3594a68fbdd" and "b29d1a1ef1d8d158c66346bc5976c9994af351f6" have entirely different histories.
90e886c62d
...
b29d1a1ef1
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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](_))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
Loading…
Reference in New Issue