From: TheSaminator Date: Sat, 12 Feb 2022 13:23:12 +0000 (-0500) Subject: Implement ship selling and refactor input validation X-Git-Url: https://gitweb.starshipfights.net/?a=commitdiff_plain;h=ed9f3b487ae9bf3823e2bce795e18f8a7e1d9f96;p=starship-fights Implement ship selling and refactor input validation --- diff --git a/src/jvmMain/kotlin/starshipfights/auth/providers.kt b/src/jvmMain/kotlin/starshipfights/auth/providers.kt index 9729fc9..b4d4d04 100644 --- a/src/jvmMain/kotlin/starshipfights/auth/providers.kt +++ b/src/jvmMain/kotlin/starshipfights/auth/providers.kt @@ -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(it) }!! + val shipId = call.parameters["ship"]?.let { Id(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(it) }!! + val shipId = call.parameters["ship"]?.let { Id(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()) } diff --git a/src/jvmMain/kotlin/starshipfights/info/views_user.kt b/src/jvmMain/kotlin/starshipfights/info/views_user.kt index 4a4d52b..60bb6b7 100644 --- a/src/jvmMain/kotlin/starshipfights/info/views_user.kt +++ b/src/jvmMain/kotlin/starshipfights/info/views_user.kt @@ -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(it) }!! + val shipId = parameters["ship"]?.let { Id(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(it) }!! + val shipId = parameters["ship"]?.let { Id(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(it) }!! diff --git a/src/jvmMain/resources/static/style.css b/src/jvmMain/resources/static/style.css index 0ed0dec..fec0705 100644 --- a/src/jvmMain/resources/static/style.css +++ b/src/jvmMain/resources/static/style.css @@ -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; +}