Finish game password protection.

This commit is contained in:
Gareth Latty
2017-03-11 16:34:25 +00:00
parent 05583b9f34
commit eaf8e15865
21 changed files with 185 additions and 69 deletions
+1 -1
View File
@@ -2,7 +2,7 @@
"name": "Massive Decks",
"repository": "https://github.com/Lattyware/massivedecks",
"env": {
"ELM_VERSION": "0.17",
"ELM_VERSION": "0.18",
"ELM_COMPILE": "elm make client/src/MassiveDecks.elm --output public/javascripts/MassiveDecks.js"
},
"buildpacks": [
+1 -1
View File
@@ -32,7 +32,7 @@ h2 {
}
}
.deck-id-input {
.input-with-button {
display: flex;
& > .mui-textfield {
+4
View File
@@ -360,6 +360,10 @@ ol.played {
opacity: 0.3;
}
&.you {
font-style: italic;
}
&.skipping {
opacity: 0.6;
}
+2 -1
View File
@@ -72,6 +72,7 @@ class API @Inject()(store: LobbyStore) (implicit context: ExecutionContext) exte
case "enableRule" => lobby.enableRule(secret, extractCommandArgument((request.body \ "rule").validate[String]))
case "disableRule" => lobby.disableRule(secret, extractCommandArgument((request.body \ "rule").validate[String]))
case "redraw" => lobby.redraw(secret)
case "setPassword" => lobby.setPassword(secret, (request.body \ "password").asOpt[String])
case unknown => throw BadRequestException(Errors.InvalidCommand(unknown))
}
}
@@ -82,7 +83,7 @@ class API @Inject()(store: LobbyStore) (implicit context: ExecutionContext) exte
wrap {
val name = (request.body \ "name").validate[String].getOrElse(throw BadRequestException(Errors.InvalidName()))
store.performInLobby(gameCode) { lobby =>
Json.toJson(lobby.newPlayer(name))
Json.toJson(lobby.newPlayer(name, (request.body \ "password").asOpt[String]))
}
}
}
+1 -5
View File
@@ -28,11 +28,7 @@ class Config(notifiers: Notifiers) {
}
def setPassword(newPass: Option[String]) = {
if (newPass.forall(pw => pw.isEmpty)) {
password = None
} else {
password = newPass
}
password = newPass.flatMap(pass => if (pass.isEmpty) None else newPass)
notifiers.configChange(config)
}
+30 -10
View File
@@ -12,7 +12,7 @@ import massivedecks.models.{Errors, Player, Game => GameModel, Lobby => LobbyMod
import massivedecks.models.Game.Formatters._
import massivedecks.models.Lobby.Formatters._
import play.api.libs.iteratee.{Enumerator, Iteratee}
import play.api.libs.json.{JsValue, Json}
import play.api.libs.json.{JsValue, JsObject, Json}
import massivedecks.Util
import massivedecks.cardcast.CardcastAPI
@@ -67,7 +67,7 @@ class Lobby(cardcast: CardcastAPI, val gameCode: String, ownerName: String)(impl
case Some(g) => GameModel.State.Playing(g.round)
}
val owner = newPlayer(ownerName)
val owner = newPlayer(ownerName, None)
/**
* @return The model for the lobby.
@@ -80,8 +80,10 @@ class Lobby(cardcast: CardcastAPI, val gameCode: String, ownerName: String)(impl
* @param name The name for the player.
* @return The secret for the player.
* @throws BadRequestException with key "name-in-use" if there is a player in the lobby with the same name.
* @throws ForbiddenException with key "password-wrong" if the password is required and wrong.
*/
def newPlayer(name: String): Player.Secret = {
def newPlayer(name: String, password: Option[String]): Player.Secret = {
ForbiddenException.verify(config.password.isEmpty || config.password == password, Errors.PasswordWrong())
val secret = players.addPlayer(name)
if (game.isDefined) {
game.get.addPlayer(secret.id)
@@ -97,7 +99,7 @@ class Lobby(cardcast: CardcastAPI, val gameCode: String, ownerName: String)(impl
* @param playCode The cardcast play code for the deck.
* @return An empty response.
* @throws ForbiddenException with key "secret-wrong-or-not-a-player" if the secret is invalid.
* @throws ForbiddenException with key "secret-wrong-or-not-a-player" if the secret is invalid.
* @throws ForbiddenException with key "not-owner" if the requester is not the owner.
* @throws RequestFailedException with the key "cardcast-timeout" if the request to cardcast doesn't complete.
*/
def addDeck(secret: Player.Secret, playCode: String): JsValue = {
@@ -113,7 +115,7 @@ class Lobby(cardcast: CardcastAPI, val gameCode: String, ownerName: String)(impl
case Failure(exception) =>
throw RequestFailedException(Errors.CardcastTimeout())
}
Json.toJson("")
EmptyResponse
}
/**
@@ -188,7 +190,7 @@ class Lobby(cardcast: CardcastAPI, val gameCode: String, ownerName: String)(impl
val game = validateInGame()
game.choose(secret.id, winner)
beginRound()
Json.toJson("")
EmptyResponse
}
/**
@@ -284,7 +286,7 @@ class Lobby(cardcast: CardcastAPI, val gameCode: String, ownerName: String)(impl
}
}
game.skip(secret.id, playerIds)
Json.toJson("")
EmptyResponse
}
/**
@@ -298,7 +300,7 @@ class Lobby(cardcast: CardcastAPI, val gameCode: String, ownerName: String)(impl
def back(secret: Player.Secret): JsValue = {
players.validateSecret(secret)
players.back(secret.id)
Json.toJson("")
EmptyResponse
}
/**
@@ -331,7 +333,23 @@ class Lobby(cardcast: CardcastAPI, val gameCode: String, ownerName: String)(impl
players.validateSecret(secret)
validateIsOwner(secret)
config.addHouseRule(rule)
Json.toJson("")
EmptyResponse
}
/**
* Set the password
*
* @param secret The secret of the player making the request.
* @param password The password to set, empty means no password.
* @return An empty response.
* @throws ForbiddenException with key "secret-wrong-or-not-a-player" if the secret is invalid.
* @throws ForbiddenException with key "not-owner" if the requester is not the owner.
*/
def setPassword(secret: Player.Secret, password: Option[String]): JsValue = {
players.validateSecret(secret)
validateIsOwner(secret)
config.setPassword(password)
EmptyResponse
}
/**
@@ -347,9 +365,11 @@ class Lobby(cardcast: CardcastAPI, val gameCode: String, ownerName: String)(impl
players.validateSecret(secret)
validateIsOwner(secret)
config.removeHouseRule(rule)
Json.toJson("")
EmptyResponse
}
def EmptyResponse: JsValue = JsObject(Seq())
/**
* Register a websocket connection.
*
+1
View File
@@ -26,6 +26,7 @@ object Errors {
case class NameInUse(error: String = "name-in-use") extends SimpleErrorDetails
case class BadlyFormedSecret(error: String = "badly-formed-secret") extends SimpleErrorDetails
case class SecretWrongOrNotAPlayer(error: String = "secret-wrong-or-not-a-player") extends SimpleErrorDetails
case class PasswordWrong(error: String = "password-wrong") extends SimpleErrorDetails
case class NotAPlayer(error: String = "not-a-player") extends SimpleErrorDetails
case class NotBeingSkipped(error: String = "not-being-skipped") extends SimpleErrorDetails
case class AlreadyLeftGame(error: String = "already-left-game") extends SimpleErrorDetails
+2 -3
View File
@@ -3,9 +3,8 @@
<head>
@head("Massive Decks", url)
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.6.3/css/font-awesome.min.css"
integrity="sha384-T8Gy5hrqNKT+hzMclPo118YTQO6cYprQmhrYwIiQ/3axmI1hQomh7Ud2hPOy8SP1"
crossorigin="anonymous">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css"
integrity="sha256-eZrrJcwDc/3uDhsdt61sL2oOBY362qM3lon1gyExkL0=" crossorigin="anonymous" />
<script src="@routes.Assets.versioned("javascripts/qrcode.min.js")"></script>
+12 -3
View File
@@ -25,18 +25,20 @@ createLobby name =
-}
type NewPlayerError
= NameInUse
| PasswordWrong
| NewPlayerLobbyNotFound
{-| Makes a request to add a new player to the given lobby. On success, returns a `Secret` for that player.
-}
newPlayer : String -> String -> Request NewPlayerError Player.Secret
newPlayer gameCode name =
newPlayer : String -> String -> String -> Request NewPlayerError Player.Secret
newPlayer gameCode name password =
request
"POST"
("/api/lobbies/" ++ gameCode ++ "/players")
(Just (encodeName name))
(Just (encodeNameAndPassword name password))
[ ( ( 400, "name-in-use" ), Decode.succeed NameInUse )
, ( ( 403, "password-wrong" ), Decode.succeed PasswordWrong )
, ( ( 404, "lobby-not-found" ), Decode.succeed NewPlayerLobbyNotFound )
]
playerSecretDecoder
@@ -279,6 +281,13 @@ disableRule rule gameCode secret =
commandRequest "disableRule" [ ( "rule", rule |> HouseRule.toString |> Encode.string ) ] [] (Decode.succeed ()) gameCode secret
{-| Make a request to set the lobby password
-}
setPassword : String -> Player.Secret -> String -> Request Never ()
setPassword gameCode secret password =
commandRequest "setPassword" [ ( "password", password |> Encode.string ) ] [] (Decode.succeed ()) gameCode secret
{- Private -}
+8 -3
View File
@@ -128,13 +128,18 @@ update message model =
if (identity == model.identity) then
case change of
Changed value ->
( { model | value = value }, Cmd.none )
( { model
| value = value
, error = Nothing
}
, Cmd.none
)
Error error ->
( { model | error = error }, Cmd.none )
Submit ->
( model, model.submit )
( { model | error = Nothing }, model.submit )
SetEnabled enabled ->
( { model | enabled = enabled }, Cmd.none )
@@ -145,4 +150,4 @@ update message model =
NoOp ->
( model, Cmd.none )
else
( model, Cmd.none )
( { model | error = Nothing }, Cmd.none )
+1 -1
View File
@@ -25,7 +25,7 @@ type alias GameCode =
type alias Config =
{ decks : List DeckInfo
, houseRules : List HouseRule.Id
, pasword : Maybe String
, password : Maybe String
}
@@ -36,3 +36,11 @@ encodePlayerId playerId =
encodeName : String -> Json.Value
encodeName name =
Json.object [ ( "name", Json.string name ) ]
encodeNameAndPassword : String -> String -> Json.Value
encodeNameAndPassword name password =
Json.object
[ ( "name", Json.string name )
, ( "password", Json.string password )
]
+35 -9
View File
@@ -10,18 +10,34 @@ import MassiveDecks.Scenes.Lobby.Models as Lobby
import MassiveDecks.Scenes.Config.Messages exposing (ConsumerMessage(..), Message(..), InputId(..), Deck(..))
import MassiveDecks.Scenes.Config.Models exposing (Model)
import MassiveDecks.Scenes.Config.UI as UI
import MassiveDecks.Models.Game as Game
import MassiveDecks.Models.Player as Player
import MassiveDecks.Util as Util exposing ((:>))
{-| Create the initial model for the config screen.
-}
init : Model
init =
{ decks = []
, deckIdInput = Input.initWithExtra DeckId "deck-id-input" UI.deckIdInputLabel "" "Play Code" UI.addDeckButton (Util.cmd AddDeck) InputMessage
, passwordInput = Input.init Password "password-input" UI.passwordInputLabel "" "Password" (Cmd.none) InputMessage
, loadingDecks = []
}
init : Game.Lobby -> Player.Secret -> ( Model, Cmd ConsumerMessage )
init lobby secret =
let
canChangeConfig =
lobby.owner == secret.id
config =
lobby.config
setPasswordButton =
if canChangeConfig then
UI.setPasswordButton
else
(\_ -> [])
in
{ decks = []
, deckIdInput = Input.initWithExtra DeckId "input-with-button" UI.deckIdInputLabel "" "Play Code" UI.addDeckButton (Util.cmd AddDeck) InputMessage
, passwordInput = Input.initWithExtra Password "input-with-button" UI.passwordInputLabel (config.password |> Maybe.withDefault "") "Password" setPasswordButton (Util.cmd SetPassword) InputMessage
, loadingDecks = []
}
! [ ( Password, Input.SetEnabled canChangeConfig ) |> InputMessage |> LocalMessage |> Util.cmd ]
{-| Subscriptions for the config screen.
@@ -77,10 +93,17 @@ update message lobbyModel =
InputMessage message ->
let
( deckIdInput, msg ) =
( deckIdInput, deckIdMsg ) =
Input.update message lobbyModel.config.deckIdInput
( passwordInput, passwordMsg ) =
Input.update message lobbyModel.config.passwordInput
in
( { model | deckIdInput = deckIdInput }, Cmd.map LocalMessage msg )
{ model
| deckIdInput = deckIdInput
, passwordInput = passwordInput
}
! [ Cmd.map LocalMessage deckIdMsg, Cmd.map LocalMessage passwordMsg ]
AddAi ->
( model, Request.send_ (API.newAi gameCode secret) ErrorMessage (\_ -> LocalMessage NoOp) )
@@ -94,6 +117,9 @@ update message lobbyModel =
DisableRule rule ->
( model, Request.send_ (API.disableRule rule gameCode secret) ErrorMessage ignore )
SetPassword ->
( model, Request.send_ (API.setPassword gameCode secret model.passwordInput.value) ErrorMessage ignore )
NoOp ->
( model, Cmd.none )
@@ -24,6 +24,7 @@ type Message
| StartGame
| EnableRule HouseRule.Id
| DisableRule HouseRule.Id
| SetPassword
| NoOp
+14 -3
View File
@@ -1,4 +1,4 @@
module MassiveDecks.Scenes.Config.UI exposing (view, deckIdInputLabel, passwordInputLabel, addDeckButton)
module MassiveDecks.Scenes.Config.UI exposing (view, deckIdInputLabel, passwordInputLabel, addDeckButton, setPasswordButton)
import String
import Html exposing (..)
@@ -94,18 +94,29 @@ infoBar =
passwordInputLabel : List (Html msg)
passwordInputLabel =
[ text "If blank, no password will be needed - anyone with the game code can play." ]
[ text "If blank, anyone with the game code can join." ]
password : Input.Model InputId Message -> Html Message
password passwordInputModel =
div []
[ h3 [] [ Icon.icon "key", text " Privacy" ]
, p [] [ text "A password that players will need to enter to get in the game." ]
, p [] [ text "A password that players will need to enter to get in the game. People already in the game will not need to enter it, and anyone in the game will be able to see it." ]
, Input.view passwordInputModel
]
setPasswordButton : String -> List (Html Message)
setPasswordButton password =
[ button
[ class "mui-btn mui-btn--small mui-btn--primary"
, onClick SetPassword
, title "Set the password."
]
[ Icon.icon "lock" ]
]
invite : String -> String -> Html msg
invite appUrl lobbyId =
let
+17 -13
View File
@@ -34,19 +34,23 @@ import MassiveDecks.Util as Util exposing ((:>))
-}
init : Init -> Game.LobbyAndHand -> Player.Secret -> ( Model, Cmd ConsumerMessage )
init init lobbyAndHand secret =
{ lobby = lobbyAndHand.lobby
, hand = lobbyAndHand.hand
, config = Config.init
, playing = Playing.init init
, browserNotifications = BrowserNotifications.init init.browserNotificationsSupported False
, secret = secret
, init = init
, notification = Nothing
, qrNeedsRendering = False
, sidebar = Sidebar.init 768 {- Should match the enhance-width in less. -}
, tts = TTS.init
}
! []
let
( config, configCmd ) =
Config.init lobbyAndHand.lobby secret
in
{ lobby = lobbyAndHand.lobby
, hand = lobbyAndHand.hand
, config = config
, playing = Playing.init init
, browserNotifications = BrowserNotifications.init init.browserNotificationsSupported False
, secret = secret
, init = init
, notification = Nothing
, qrNeedsRendering = False
, sidebar = Sidebar.init 768 {- Should match the enhance-width in less. -}
, tts = TTS.init
}
! [ configCmd |> Cmd.map (ConfigMessage >> LocalMessage) ]
{-| Subscriptions for the lobby.
+15 -11
View File
@@ -130,20 +130,24 @@ score client owner player =
[ ( Player.statusName player.status, True )
, ( "disconnected", player.disconnected )
, ( "left", player.left )
, ( "you", player.id == client )
]
prename =
((if player.id == owner then
[ Icon.icon "star", text " " ]
else
[]
)
++ (if player.disconnected then
[ Icon.icon "minus-circle", text " " ]
else
[]
)
)
List.concat
[ if player.id == owner then
[ Icon.icon "star", text " " ]
else
[]
, if player.id == client then
[ Icon.icon "user-circle", text " " ]
else
[]
, if player.disconnected then
[ Icon.icon "minus-circle", text " " ]
else
[]
]
afterNameTitle =
(if player.id == owner then
+17 -2
View File
@@ -40,6 +40,8 @@ init init location =
, path = path
, nameInput = Input.init Name "name-input" [ text "Your name in the game." ] "" "Nickname" (Util.cmd SubmitCurrentTab) InputMessage
, gameCodeInput = Input.init GameCode "game-code-input" [ text "The code for the game to join." ] (path.gameCode |> Maybe.withDefault "") "" (Util.cmd JoinLobbyAsNewPlayer) InputMessage
, passwordInput = Input.init Password "password-input" [ text "The password for the game to join." ] "" "Password" (Util.cmd JoinLobbyAsNewPlayer) InputMessage
, passwordRequired = Nothing
, errors = Errors.init
, overlay = Overlay.init OverlayMessage
, buttonsEnabled = True
@@ -182,6 +184,9 @@ update message model =
SetButtonsEnabled enabled ->
( { model | buttonsEnabled = enabled }, Cmd.none )
SetPasswordRequired ->
( { model | passwordRequired = Just model.gameCodeInput.value }, Cmd.none )
JoinLobbyAsNewPlayer ->
( { model | buttonsEnabled = False }, Util.cmd (JoinGivenLobbyAsNewPlayer model.gameCodeInput.value) )
@@ -189,7 +194,7 @@ update message model =
case List.filter (.gameCode >> ((==) gameCode)) model.storage |> List.head of
Nothing ->
model
! [ Request.send (API.newPlayer gameCode model.nameInput.value)
! [ Request.send (API.newPlayer gameCode model.nameInput.value model.passwordInput.value)
newPlayerErrorHandler
ErrorMessage
(StoreCredentialsAndMoveToLobby gameCode)
@@ -237,12 +242,16 @@ update message model =
( gameCodeInput, gameCodeCmd ) =
Input.update message model.gameCodeInput
( passwordInput, passwordCmd ) =
Input.update message model.passwordInput
in
( { model
| nameInput = nameInput
, gameCodeInput = gameCodeInput
, passwordInput = passwordInput
}
, Cmd.batch [ nameCmd, gameCodeCmd ]
, Cmd.batch [ nameCmd, gameCodeCmd, passwordCmd ]
)
OverlayMessage overlayMessage ->
@@ -319,6 +328,12 @@ newPlayerErrorHandler error =
API.NameInUse ->
( Name, Just "This name is already in use in this game, try something else." |> Input.Error ) |> InputMessage
API.PasswordWrong ->
Batch
[ SetPasswordRequired
, ( Password, Just "This game requires a password, please check you have the right one." |> Input.Error ) |> InputMessage
]
API.NewPlayerLobbyNotFound ->
( GameCode, Just "This game doesn't exist - check you have the right code." |> Input.Error ) |> InputMessage
in
@@ -32,6 +32,7 @@ type Message
| OverlayMessage (Overlay.Message Message)
| TabsMessage (Tabs.Message Tab)
| StorageMessage Storage.Message
| SetPasswordRequired
| Batch (List Message)
| NoOp
@@ -41,6 +42,7 @@ type Message
type InputId
= Name
| GameCode
| Password
{-| Tabs for the start page.
@@ -18,6 +18,8 @@ type alias Model =
, path : Path
, nameInput : Input.Model InputId Message
, gameCodeInput : Input.Model InputId Message
, passwordInput : Input.Model InputId Message
, passwordRequired : Maybe String
, errors : Errors.Model
, overlay : Overlay.Model Message
, buttonsEnabled : Bool
+11 -3
View File
@@ -97,9 +97,17 @@ renderTab nameEntered gameCodeEntered model tab =
[ createLobbyButton (nameEntered && model.buttonsEnabled) ]
Join ->
[ Input.view model.gameCodeInput
, joinLobbyButton (nameEntered && gameCodeEntered && model.buttonsEnabled)
]
List.concat
[ [ Input.view model.gameCodeInput
]
, (if model.passwordRequired == (Just model.gameCodeInput.value) then
[ Input.view model.passwordInput ]
else
[]
)
, [ joinLobbyButton (nameEntered && gameCodeEntered && model.buttonsEnabled)
]
]
existingGames : List Game.GameCodeAndSecret -> List (Html msg)