Compare commits

...

3 Commits

Author SHA1 Message Date
efim 867b2f0f20 auth giving out actual random seesionId 2023-04-28 10:11:36 +04:00
efim 62d63546c4 adding alpha create room capability 2023-04-28 10:08:04 +04:00
efim e1364c9b9b simple routes mapped to room service 2023-04-28 09:28:57 +04:00
4 changed files with 157 additions and 27 deletions

View File

@ -27,22 +27,22 @@ trait Auth[F[_]] {
*/
def joinRoom(roomId: RoomID, playerId: PlayerID): F[ResponseCookie]
def deleteSession(sessionId: Long): F[Unit]
def deleteSession(sessionId: Long): F[ResponseCookie]
}
object Auth {
type SessionsMap = Map[Long, (RoomID, PlayerID)]
class SimpleAuth[F[_]: Sync](sessions: Ref[F, SessionsMap]) extends Auth[F] {
// TODO make "remove session usable from authed route"
val authcookieName = "authcookie"
val authcookieName = "authcookie"
class SimpleAuth[F[_]: Sync](sessions: Ref[F, SessionsMap]) extends Auth[F] {
override def joinRoom(roomId: RoomID, playerId: PlayerID): F[ResponseCookie] = {
// TODO check for existing session for same room
// and do i want to logout if existing session for another room? ugh
// newSessionId = Random.nextLong() // TODO return after i stop mocking RoomService
val newSessionId = TestModels.testSessionId
val newSessionId = Random.nextLong()
sessions
.update(_.updated(newSessionId, (roomId, playerId)))
.as(
@ -67,8 +67,16 @@ object Auth {
}
}
override def deleteSession(sessionId: Long): F[Unit] = {
sessions.update(_.removed(sessionId))
override def deleteSession(sessionId: Long): F[ResponseCookie] = {
sessions
.update(_.removed(sessionId))
.as(
ResponseCookie(
name = authcookieName,
content = "",
secure = true
).clearCookie
)
}
}

View File

@ -22,9 +22,12 @@ object MyHttpService {
val authedRoomRoutes: AuthedRoutes[(PlayerID, RoomID), IO] =
AuthedRoutes.of {
case GET -> Root / "subscribe" as (playerId, roomId) => {
val initial = Stream.evals(roomService.getRoom(roomId))
val subscription = roomService.subscribe(roomId)
val send: Stream[IO, WebSocketFrame] =
roomService
.subscribe(roomId)
(initial ++ subscription)
.evalTap(state => IO(println(s">> sending room state $state to $playerId")))
.map(state => WebSocketFrame.Text(state.getViewFor(playerId).asJson.noSpaces))
val receive: Pipe[IO, WebSocketFrame, Unit] = _.evalMap {
@ -35,17 +38,23 @@ object MyHttpService {
wsb.build(send, receive)
}
case GET -> Root / "vote" / vote as (playerId, roomId) => {
// TODO forward these to the service implementation
IO(println(s">> got $vote from $playerId in $roomId")) >> Ok()
IO(println(s">> got $vote from $playerId in $roomId")) >>
roomService.acceptVote(roomId, playerId, vote) >> Ok()
}
case GET -> Root / "end-voting" as (playerId, roomId) => {
IO(println(s">> got request to end voting from $playerId in $roomId")) >> Ok()
IO(println(s">> got request to end voting from $playerId in $roomId")) >>
roomService.endVoting(roomId, playerId) >> Ok()
}
case GET -> Root / "new-poll" as (playerId, roomId) => {
IO(println(s">> got request to start new voting from $playerId in $roomId")) >> Ok()
IO(println(s">> got request to start new voting from $playerId in $roomId")) >>
roomService.startNewPoll(roomId, playerId) >> Ok()
}
case GET -> Root / "logout" as (playerId, roomId) => {
IO(println(s">> got request to logout from $playerId in $roomId")) >> Ok()
for {
_ <- IO(println(s">> got request to logout from $playerId in $roomId"))
_ <- roomService.leaveRoom(roomId, playerId)
cookie = ResponseCookie(name = Auth.authcookieName, content = "").clearCookie
} yield (Response(Status.Ok).addCookie(cookie))
}
}
@ -63,13 +72,32 @@ object MyHttpService {
playerId <- EitherT(roomService.joinRoom(roomId, nickName, nickPassword, roomPassword))
authCookie <- EitherT.liftF(auth.joinRoom(roomId, playerId))
_ <- EitherT.liftF(IO(println(s"> logging in $nickName to $roomName")))
resp <- EitherT.liftF(Ok().flatTap(resp => IO(resp.addCookie(authCookie))))
resp = Response(Status.Ok).addCookie(authCookie)
} yield resp
val response = responseOrError.leftSemiflatMap(error => Forbidden(error.toString())).merge
response
}
case req @ POST -> Root / "create-room" => {
val responseOrError = for {
data <- EitherT.right(req.as[Requests.LogIn])
Requests.LogIn(roomName, nickName, roomPassword, nickPassword) = data
room <- EitherT(roomService.createRoom(roomName, nickName, nickPassword, roomPassword))
owner = room.players.head // TODO add check
authCookie <- EitherT.liftF(auth.joinRoom(room.id, owner.id))
_ <- EitherT.liftF(
IO(println(s"> logging in $nickName to new room $room | $authCookie"))
)
resp = Response(Status.Ok).addCookie(authCookie)
_ <- EitherT.liftF(IO(println(s"> about to reply $resp ${resp.cookies}")))
} yield resp
val response = responseOrError.leftSemiflatMap(error => Forbidden(error.toString())).merge
response
}
}
(authenticationRoute <+> authMiddleware(authedRoomRoutes)).orNotFound

View File

@ -15,14 +15,23 @@ enum RoomError {
}
trait RoomService[F[_]] {
def createRoom(newRoom: Room): F[Either[RoomError, Room]]
def updateRoom(room: Room): F[Unit]
def createRoom(
roomName: String,
nickName: String,
nickPassword: String,
roomPassword: String
): F[Either[RoomError, Room]]
def updateRoom(roomId: RoomID, roomUpd: Room => Room): F[Unit]
def joinRoom(
id: RoomID,
nickName: String,
nickPassword: String,
roomPassword: String
): F[Either[RoomError, PlayerID]]
def acceptVote(roomID: RoomID, playerID: PlayerID, vote: String): F[Unit]
def endVoting(roomID: RoomID, playerID: PlayerID): F[Unit]
def startNewPoll(roomID: RoomID, playerID: PlayerID): F[Unit]
def leaveRoom(roomID: RoomID, playerID: PlayerID): F[Unit]
def deleteRoom(roomID: RoomID): F[Unit]
def getRoom(roomID: RoomID): F[Option[Room]]
def subscribe(roomID: RoomID): Stream[F, Room]
@ -31,33 +40,91 @@ trait RoomService[F[_]] {
class InMemoryRoomService[F[_]: Concurrent](stateRef: Ref[F, Map[RoomID, (Room, Topic[F, Room])]])
extends RoomService[F] {
override def createRoom(newRoom: Room): F[Either[RoomError, Room]] = {
// TODO accept allowed cards and separate request
override def createRoom(
roomName: String,
nickName: String,
nickPassword: String,
roomPassword: String
): F[Either[RoomError, Room]] = {
for {
updatesTopic <- Topic[F, Room]
room <- stateRef.modify { rooms =>
rooms.get(newRoom.id) match {
val roomId = RoomID(roomName)
rooms.get(roomId) match {
case Some(_) =>
rooms -> RoomError.RoomAlreadyExists(newRoom.id.name).asLeft[Room]
rooms -> RoomError.RoomAlreadyExists(roomName).asLeft[Room]
case None =>
(rooms.updated(newRoom.id, (newRoom, updatesTopic))) -> newRoom.asRight[RoomError]
val ownerPlayer = Player.create(nickName)
val newRoom = Room(
roomId,
players = List(ownerPlayer),
owner = ownerPlayer.id,
password = roomPassword,
allowedCards = List("XS", "S", "M", "L", "XL"), // TODO accept from front
round = RoundState.Voting(Map.empty),
playersPasswords = Map(nickName -> nickPassword)
)
rooms.updated(newRoom.id, (newRoom, updatesTopic)) -> newRoom.asRight[RoomError]
}
}
} yield room
}
override def updateRoom(room: Room): F[Unit] = {
override def updateRoom(roomId: RoomID, roomUpd: Room => Room): F[Unit] = {
for {
// modify is function to update state and compute auxillary value to return, here - topic
topic <- stateRef.modify[Topic[F, Room]] { state =>
state.get(room.id) match {
case Some((oldRoom, topic)) => state.updated(room.id, (room, topic)) -> topic
publishUpd <- stateRef.modify[F[Unit]] { state =>
state.get(roomId) match {
case Some((oldRoom, topic)) =>
val newRoom = roomUpd(oldRoom)
state.updated(roomId, (newRoom, topic)) -> topic.publish1(newRoom).void
case None =>
throw new IllegalStateException(s"updateRoom with ${room.id} on nonexistent room")
throw new IllegalStateException(s"updateRoom with $roomId on nonexistent room")
}
}
_ <- topic.publish1(room) // update and publish are not atomic, sadly races can happen
_ <-
publishUpd // update and publish are not atomic, sadly races can happen (TODO use atomic ref)
} yield ()
}
override def acceptVote(roomID: RoomID, playerID: PlayerID, vote: String): F[Unit] =
updateRoom(
roomID,
room =>
room.round match {
case RoundState.Viewing(_) => room
case RoundState.Voting(votes) =>
if (room.allowedCards.contains(vote))
room.copy(round = RoundState.Voting(votes.updated(playerID, vote)))
else room
}
)
// TODO check permission
override def endVoting(roomID: RoomID, playerID: PlayerID): F[Unit] = updateRoom(
roomID,
room =>
room.round match {
case RoundState.Viewing(_) => room
case RoundState.Voting(votes) => room.copy(round = RoundState.Viewing(votes))
}
)
override def startNewPoll(roomID: RoomID, playerID: PlayerID): F[Unit] = updateRoom(
roomID,
room =>
room.round match {
case RoundState.Viewing(_) => room.copy(round = RoundState.Voting(Map.empty))
case RoundState.Voting(votes) => room
}
)
/** removes player from the active players keeps information on nick password, if one was present
*/
override def leaveRoom(roomID: RoomID, playerID: PlayerID): F[Unit] = updateRoom(
roomID,
room => room.copy(players = room.players.filterNot(_.id == playerID))
)
override def deleteRoom(roomID: RoomID): F[Unit] = {
for {
topic <- stateRef.modify[Topic[F, Room]](state =>

View File

@ -76,6 +76,32 @@ object JoinRoomComponent {
}
} --> responseReceived
)
val newRoomButton = button(
"Create new room",
onClick
.mapTo {
(roomNameVar.now(), roomPassVar.now(), nicknameVar.now(), nicknamePass.now())
}
.flatMap { case (roomName, roomPass, nickname, nicknamePass) =>
Fetch
.post(
"/api/create-room",
body = Requests.LogIn(
roomName,
nickname,
roomPass,
nicknamePass
)
)
.text.map { response =>
if (response.ok) {
loggedIn.onNext(true)
response
} else response
}
} --> responseReceived
)
div(
className := "flex flex-col h-full justify-center",
@ -85,6 +111,7 @@ object JoinRoomComponent {
nameInput(nicknameVar, "Enter your nickname:"),
passInput(nicknamePass, "nickname pass:"),
submitButton,
newRoomButton,
div(
div(
code("received:")