Implement ship selling and refactor input validation
authorTheSaminator <TheSaminator@users.noreply.github.com>
Sat, 12 Feb 2022 13:23:12 +0000 (08:23 -0500)
committerTheSaminator <TheSaminator@users.noreply.github.com>
Sat, 12 Feb 2022 13:23:12 +0000 (08:23 -0500)
src/jvmMain/kotlin/starshipfights/auth/providers.kt
src/jvmMain/kotlin/starshipfights/info/views_user.kt
src/jvmMain/resources/static/style.css

index 9729fc92479a84487a9f1c35de370d4f5139a4dd..b4d4d041c9c75ffe56188b8128f5a1edc7ac8a0c 100644 (file)
@@ -26,14 +26,16 @@ import starshipfights.data.admiralty.*
 import starshipfights.data.auth.User
 import starshipfights.data.auth.UserSession
 import starshipfights.data.createNonce
-import starshipfights.game.Faction
-import starshipfights.game.ShipType
-import starshipfights.game.buyPrice
-import starshipfights.game.toUrlSlug
+import starshipfights.game.*
 import starshipfights.info.*
 import java.time.Instant
 import java.time.temporal.ChronoUnit
 
+const val PROFILE_NAME_MAX_LENGTH = 32
+const val PROFILE_BIO_MAX_LENGTH = 240
+const val ADMIRAL_NAME_MAX_LENGTH = 48
+const val SHIP_NAME_MAX_LENGTH = 48
+
 interface AuthProvider {
        fun installApplication(app: Application) = Unit
        fun installAuth(conf: Authentication.Configuration)
@@ -86,8 +88,8 @@ interface AuthProvider {
                                        val form = call.receiveParameters()
                                        
                                        val newUser = currentUser.copy(
-                                               profileName = form["name"]?.takeIf { it.isNotBlank() && it.length <= 32 } ?: redirect("/me/manage?" + parametersOf("error", "Invalid name - must not be blank, must be at most 32 characters").formUrlEncode()),
-                                               profileBio = form["bio"]?.takeIf { it.isNotBlank() && it.length <= 240 } ?: redirect("/me/manage?" + parametersOf("error", "Invalid bio - must not be blank, must be at most 240 characters").formUrlEncode())
+                                               profileName = form["name"]?.takeIf { it.isNotBlank() && it.length <= PROFILE_NAME_MAX_LENGTH } ?: redirect("/me/manage?" + parametersOf("error", "Invalid name - must not be blank, must be at most $PROFILE_NAME_MAX_LENGTH characters").formUrlEncode()),
+                                               profileBio = form["bio"]?.takeIf { it.isNotBlank() && it.length <= PROFILE_BIO_MAX_LENGTH } ?: redirect("/me/manage?" + parametersOf("error", "Invalid bio - must not be blank, must be at most $PROFILE_BIO_MAX_LENGTH characters").formUrlEncode())
                                        )
                                        User.put(newUser)
                                        redirect("/user/${newUser.id}")
@@ -107,7 +109,7 @@ interface AuthProvider {
                                        
                                        val newAdmiral = Admiral(
                                                owningUser = currentUser,
-                                               name = form["name"]?.takeIf { it.isNotBlank() } ?: throw MissingRequestParameterException("name"),
+                                               name = form["name"]?.takeIf { it.isNotBlank() && it.length < ADMIRAL_NAME_MAX_LENGTH } ?: throw MissingRequestParameterException("name"),
                                                isFemale = form.getOrFail("sex") == "female",
                                                faction = Faction.valueOf(form.getOrFail("faction")),
                                                acumen = 0,
@@ -150,6 +152,56 @@ interface AuthProvider {
                                        redirect("/admiral/$admiralId")
                                }
                                
+                               get("/admiral/{id}/rename/{ship}") {
+                                       call.respondHtml(HttpStatusCode.OK, call.renameShipPage())
+                               }
+                               
+                               post("/admiral/{id}/rename/{ship}") {
+                                       val currentUser = call.getUserSession()?.user
+                                       
+                                       val admiralId = call.parameters["id"]?.let { Id<Admiral>(it) }!!
+                                       val shipId = call.parameters["ship"]?.let { Id<ShipInDrydock>(it) }!!
+                                       
+                                       val (admiral, ship) = coroutineScope {
+                                               Admiral.get(admiralId)!! to ShipInDrydock.get(shipId)!!
+                                       }
+                                       
+                                       if (admiral.owningUser != currentUser) throw ForbiddenException()
+                                       if (ship.owningAdmiral != admiralId) throw ForbiddenException()
+                                       
+                                       val newName = call.receiveParameters()["name"]?.takeIf { it.isNotBlank() && it.length <= SHIP_NAME_MAX_LENGTH } ?: redirect("/admiral/${admiralId}/manage")
+                                       ShipInDrydock.set(shipId, setValue(ShipInDrydock::name, newName))
+                                       redirect("/admiral/${admiralId}/manage")
+                               }
+                               
+                               get("/admiral/{id}/sell/{ship}") {
+                                       call.respondHtml(HttpStatusCode.OK, call.sellShipConfirmPage())
+                               }
+                               
+                               post("/admiral/{id}/sell/{ship}") {
+                                       val currentUser = call.getUserSession()?.user
+                                       
+                                       val admiralId = call.parameters["id"]?.let { Id<Admiral>(it) }!!
+                                       val shipId = call.parameters["ship"]?.let { Id<ShipInDrydock>(it) }!!
+                                       
+                                       val (admiral, ship) = coroutineScope {
+                                               Admiral.get(admiralId)!! to ShipInDrydock.get(shipId)!!
+                                       }
+                                       
+                                       if (admiral.owningUser != currentUser) throw ForbiddenException()
+                                       if (ship.owningAdmiral != admiralId) throw ForbiddenException()
+                                       
+                                       if (ship.status != DrydockStatus.Ready) redirect("/admiral/${admiralId}/manage")
+                                       if (ship.shipType.weightClass.isUnique) redirect("/admiral/${admiralId}/manage")
+                                       
+                                       launch { ShipInDrydock.del(shipId) }
+                                       launch {
+                                               Admiral.set(admiralId, inc(Admiral::money, ship.shipType.weightClass.sellPrice))
+                                       }
+                                       
+                                       redirect("/admiral/${admiralId}/manage")
+                               }
+                               
                                get("/admiral/{id}/buy/{ship}") {
                                        call.respondHtml(HttpStatusCode.OK, call.buyShipConfirmPage())
                                }
index 4a4d52bb9b40115de17d4f24097a13fe8b8f7977..60bb6b770b708eee6a2e371a1df96ab51a53cee0 100644 (file)
@@ -13,8 +13,7 @@ import org.litote.kmongo.gt
 import org.litote.kmongo.or
 import starshipfights.CurrentConfiguration
 import starshipfights.ForbiddenException
-import starshipfights.auth.getUser
-import starshipfights.auth.getUserSession
+import starshipfights.auth.*
 import starshipfights.data.Id
 import starshipfights.data.admiralty.*
 import starshipfights.data.auth.User
@@ -137,14 +136,14 @@ suspend fun ApplicationCall.manageUserPage(): HTML.() -> Unit {
                                }
                                textInput(name = "name") {
                                        required = true
-                                       maxLength = "32"
+                                       maxLength = "$PROFILE_NAME_MAX_LENGTH"
                                        
                                        value = currentUser.profileName
                                        autoComplete = false
                                }
                                p {
                                        style = "font-style:italic;font-size:0.8em;color:#555"
-                                       +"Max length 32 characters"
+                                       +"Max length $PROFILE_NAME_MAX_LENGTH characters"
                                }
                                h3 {
                                        label {
@@ -157,7 +156,7 @@ suspend fun ApplicationCall.manageUserPage(): HTML.() -> Unit {
                                        style = "width: 100%;height:5em"
                                        
                                        required = true
-                                       maxLength = "240"
+                                       maxLength = "$PROFILE_BIO_MAX_LENGTH"
                                        
                                        +currentUser.profileBio
                                }
@@ -278,6 +277,7 @@ suspend fun ApplicationCall.createAdmiralPage(): HTML.() -> Unit {
                                        
                                        autoComplete = false
                                        required = true
+                                       maxLength = "$ADMIRAL_NAME_MAX_LENGTH"
                                }
                                p {
                                        label {
@@ -481,8 +481,6 @@ suspend fun ApplicationCall.manageAdmiralPage(): HTML.() -> Unit {
        
        val ownedShips = ShipInDrydock.select(ShipInDrydock::owningAdmiral eq admiralId).toList()
        
-       //val sellableShips = ownedShips.filter { it.status == DrydockStatus.Ready }.sortedBy { it.name }.sortedBy { it.shipType.weightClass.rank }
-       
        val buyableShips = ShipType.values().filter { type ->
                type.faction == admiral.faction && type.weightClass.rank <= admiral.rank.maxShipWeightClass.rank && type.weightClass.buyPrice <= admiral.money && (if (type.weightClass.isUnique) ownedShips.none { it.shipType.weightClass == type.weightClass } else true)
        }.sortedBy { it.name }.sortedBy { it.weightClass.rank }
@@ -509,6 +507,7 @@ suspend fun ApplicationCall.manageAdmiralPage(): HTML.() -> Unit {
                                        
                                        required = true
                                        value = admiral.name
+                                       maxLength = "$ADMIRAL_NAME_MAX_LENGTH"
                                }
                                p {
                                        label {
@@ -558,6 +557,50 @@ suspend fun ApplicationCall.manageAdmiralPage(): HTML.() -> Unit {
                }
                section {
                        h2 { +"Manage Fleet" }
+                       table {
+                               tr {
+                                       th { +"Ship Name" }
+                                       th { +"Ship Class" }
+                                       th { +"Ship Status" }
+                                       th { +"Ship Value" }
+                               }
+                               ownedShips.sortedBy { it.name }.sortedBy { it.shipType.weightClass.rank }.forEach { ship ->
+                                       tr {
+                                               td {
+                                                       +ship.shipData.fullName
+                                                       br
+                                                       a(href = "/admiral/${admiralId}/rename/${ship.id}") { +"Rename" }
+                                               }
+                                               td {
+                                                       a(href = "/info/${ship.shipData.shipType.toUrlSlug()}") {
+                                                               +ship.shipData.shipType.fullDisplayName
+                                                       }
+                                               }
+                                               td {
+                                                       when (ship.status) {
+                                                               DrydockStatus.Ready -> +"Ready"
+                                                               is DrydockStatus.InRepair -> {
+                                                                       +"Repairing"
+                                                                       br
+                                                                       +"Will be ready at "
+                                                                       span(classes = "moment") {
+                                                                               style = "display:none"
+                                                                               +ship.status.until.toEpochMilli().toString()
+                                                                       }
+                                                               }
+                                                       }
+                                               }
+                                               td {
+                                                       +ship.shipType.weightClass.sellPrice.toString()
+                                                       +" Electro-Ducats"
+                                                       if (ship.status == DrydockStatus.Ready && !ship.shipType.weightClass.isUnique) {
+                                                               br
+                                                               a(href = "/admiral/${admiralId}/sell/${ship.id}") { +"Sell" }
+                                                       }
+                                               }
+                                       }
+                               }
+                       }
                        h3 { +"Buy New Ship" }
                        table {
                                tr {
@@ -573,10 +616,8 @@ suspend fun ApplicationCall.manageAdmiralPage(): HTML.() -> Unit {
                                                        +" Electro-Ducats"
                                                }
                                                td {
-                                                       form(action = "/admiral/${admiralId}/buy/${st.toUrlSlug()}", method = FormMethod.get) {
-                                                               submitInput {
-                                                                       value = "Buy"
-                                                               }
+                                                       a(href = "/admiral/${admiralId}/buy/${st.toUrlSlug()}") {
+                                                               +"Buy"
                                                        }
                                                }
                                        }
@@ -586,6 +627,89 @@ suspend fun ApplicationCall.manageAdmiralPage(): HTML.() -> Unit {
        }
 }
 
+suspend fun ApplicationCall.renameShipPage(): HTML.() -> Unit {
+       val currentUser = getUserSession()?.user
+       
+       val admiralId = parameters["id"]?.let { Id<Admiral>(it) }!!
+       val shipId = parameters["ship"]?.let { Id<ShipInDrydock>(it) }!!
+       
+       val (admiral, ship) = coroutineScope {
+               Admiral.get(admiralId)!! to ShipInDrydock.get(shipId)!!
+       }
+       
+       if (admiral.owningUser != currentUser) throw ForbiddenException()
+       if (ship.owningAdmiral != admiralId) throw ForbiddenException()
+       
+       return page("Renaming Ship", null, null) {
+               section {
+                       h1 { +"Renaming Ship" }
+                       p {
+                               +"${admiral.fullName} is about to rename the ${ship.shipData.fullName}. Choose a name here:"
+                       }
+                       form(method = FormMethod.post, action = "/admiral/${admiral.id}/rename/${ship.id}") {
+                               textInput(name = "name") {
+                                       id = "name"
+                                       
+                                       autoComplete = false
+                                       required = true
+                                       
+                                       maxLength = "$SHIP_NAME_MAX_LENGTH"
+                               }
+                               p {
+                                       style = "font-style:italic;font-size:0.8em;color:#555"
+                                       +"Max length $SHIP_NAME_MAX_LENGTH characters"
+                               }
+                               submitInput {
+                                       value = "Rename"
+                               }
+                       }
+                       form(method = FormMethod.get, action = "/admiral/${admiral.id}/manage") {
+                               submitInput {
+                                       value = "Cancel"
+                               }
+                       }
+               }
+       }
+}
+
+suspend fun ApplicationCall.sellShipConfirmPage(): HTML.() -> Unit {
+       val currentUser = getUserSession()?.user
+       
+       val admiralId = parameters["id"]?.let { Id<Admiral>(it) }!!
+       val shipId = parameters["ship"]?.let { Id<ShipInDrydock>(it) }!!
+       
+       val (admiral, ship) = coroutineScope {
+               Admiral.get(admiralId)!! to ShipInDrydock.get(shipId)!!
+       }
+       
+       if (admiral.owningUser != currentUser) throw ForbiddenException()
+       if (ship.owningAdmiral != admiralId) throw ForbiddenException()
+       
+       if (ship.status != DrydockStatus.Ready) redirect("/admiral/${admiralId}/manage")
+       if (ship.shipType.weightClass.isUnique) redirect("/admiral/${admiralId}/manage")
+       
+       return page(
+               "Are You Sure?", null, null
+       ) {
+               section {
+                       h1 { +"Are You Sure?" }
+                       p {
+                               +"${admiral.fullName} is about to sell the ${ship.shipData.fullName} for ${ship.shipType.weightClass.sellPrice} Electro-Ducats."
+                       }
+                       form(method = FormMethod.get, action = "/admiral/${admiral.id}/manage") {
+                               submitInput {
+                                       value = "Cancel"
+                               }
+                       }
+                       form(method = FormMethod.post, action = "/admiral/${admiral.id}/sell/${ship.id}") {
+                               submitInput {
+                                       value = "Sell"
+                               }
+                       }
+               }
+       }
+}
+
 suspend fun ApplicationCall.buyShipConfirmPage(): HTML.() -> Unit {
        val currentUser = getUserSession()?.user
        val admiralId = parameters["id"]?.let { Id<Admiral>(it) }!!
index 0ed0decbd7e5b9d0710a3a2addab1c87b58d4a4d..fec07053fd4fa2ec7b31d29ea5496cdb055fee1a 100644 (file)
@@ -302,3 +302,8 @@ input[type=submit].evil:hover {
 input[type=submit].evil:active {
        background-color: #844;
 }
+
+input[type=submit]:disabled,
+input[type=submit].evil:disabled {
+       background-color: #444;
+}