Servers ❤️ Kotlin

E86c7de302b490b7f2a67e54960510d0?s=47 Ryan Harter
October 05, 2018

Servers ❤️ Kotlin

A practical introduction of building an In App Purchase verification server backend with Kotlin using Ktor from JetBrains.

E86c7de302b490b7f2a67e54960510d0?s=128

Ryan Harter

October 05, 2018
Tweet

Transcript

  1. Servers ❤ Kotlin Ryan Harter @rharter

  2. Ktor

  3. Ktor Easy to use, fun and asynchronous.

  4. Ktor Easy to use, fun and asynchronous. Composable, DSL based

    web services in Kotlin
  5. Ktor Application

  6. Ktor Application Jetty Netty Tomcat Servlet

  7. Ktor Application Feature Feature Feature Feature Servlet

  8. Ktor Application Feature Feature Feature Feature Routing HTML Templates Serialization

    Authentication Servlet
  9. Ktor

  10. Ktor Verify

  11. Ktor Verify Admin

  12. call.respond("Hello World") post("/verify") { routing { fun Application.verify() { }

    } }
  13. call.respond("Hello World") post("/verify") { routing { fun Application.verify() { }

    } }
  14. call.respond("Hello World") post("/verify") { routing { fun Application.verify() { }

    } }
  15. call.respond("Hello World") post("/verify") { routing { fun Application.verify() { }

    } }
  16. call.respond("Hello World") post("/verify") { routing { fun Application.verify() { }

    } }
  17. → curl -X POST -d '' http://localhost:8080/verify

  18. → curl -X POST -d '' http://localhost:8080/verify Hello World

  19. Typed Responses

  20. call.respond("Hello World") post("/verify") { routing { fun Application.verify() { }

    } }
  21. call.respond(Response(status = "OK")) post("/verify") { routing { fun Application.verify() {

    } } } data class Response(val status: String)
  22. → curl -X POST -d '' http://localhost:8080/verify

  23. → curl -X POST -d '' http://localhost:8080/verify →

  24. call.respond(Response(status = "OK")) post("/verify") { routing { fun Application.verify() {

    } } } data class Response(val status: String)
  25. call.respond(Response(status = "OK")) post("/verify") { routing { fun Application.verify() {

    } } } data class Response(val status: String) install(StatusPages)
  26. call.respond(Response(status = "OK")) post("/verify") { routing { fun Application.verify() {

    } } } data class Response(val status: String) install(StatusPages) { } exception<Throwable> { e -> }
  27. call.respond(Response(status = "OK")) post("/verify") { routing { fun Application.verify() {

    } } } data class Response(val status: String) install(StatusPages) { } exception<Throwable> { e -> call.respondText(e.localizedMessage, ContentType.Text.Plain, HttpStatusCode.InternalServerError) }
  28. call.respond(Response(status = "OK")) post("/verify") { routing { fun Application.verify() {

    } } } data class Response(val status: String) install(StatusPages) { } exception<Throwable> { e -> call.respondText(e.localizedMessage, ContentType.Text.Plain, HttpStatusCode.InternalServerError) }
  29. → curl -X POST -d '' http://localhost:8080/verify

  30. → curl -X POST -d '' http://localhost:8080/verify Cannot transform this

    request's content to class com.ryanharter.example.Response
  31. call.respond(Response(status = "OK")) post("/verify") { routing { fun Application.verify() {

    } } } data class Response(val status: String) install(StatusPages) { } exception<Throwable> { e -> call.respondText(e.localizedMessage, ContentType.Text.Plain, HttpStatusCode.InternalServerError) }
  32. install(StatusPages) { }1 call.respond(Response(status = "OK")) post("/verify") { routing {

    fun Application.verify() { } } } data class Response(val status: String) ...
  33. call.respond(Response(status = "OK")) post("/verify") { routing { fun Application.verify() {

    } } } data class Response(val status: String) install(ContentNegotiation) ≈ install(StatusPages) { }1 ... ≈
  34. call.respond(Response(status = "OK")) post("/verify") { routing { fun Application.verify() {

    } } } data class Response(val status: String) install(ContentNegotiation) ≈ install(StatusPages) { }1 ... ≈ { }2
  35. call.respond(Response(status = "OK")) post("/verify") { routing { fun Application.verify() {

    } } } data class Response(val status: String) install(ContentNegotiation) ≈ install(StatusPages) { }1 ... ≈ { }2 moshi() ≈ ≈
  36. call.respond(Response(status = "OK")) post("/verify") { routing { fun Application.verify() {

    } } } data class Response(val status: String) install(ContentNegotiation) ≈ install(StatusPages) { }1 ... ≈ { }2 moshi() ≈ ≈ @JsonClass(generateAdapter = true)
  37. call.respond(Response(status = "OK")) post("/verify") { routing { fun Application.verify() {

    } } } data class Response(val status: String) install(ContentNegotiation) install(StatusPages) { }1 ... { }2 moshi() @JsonClass(generateAdapter = true)
  38. → curl -X POST -d '' http://localhost:8080/verify

  39. → curl -X POST -d '' http://localhost:8080/verify {"status":"OK"}

  40. call.respond(Response(status = "OK")) post("/verify") { routing { fun Application.verify() {

    } } } data class Response(val status: String) install(ContentNegotiation) install(StatusPages) { }1 ... { }2 moshi() @JsonClass(generateAdapter = true)
  41. call.respond(Response(status = "OK")) post("/verify") { routing { fun Application.verify() {

    } } } data class Response(val status: String) install(ContentNegotiation) install(StatusPages) { }1 ... { }2 @JsonClass(generateAdapter = true) ...
  42. Typed Requests

  43. None
  44. None
  45. call.respond(Response(status = "OK")) post("/verify") { routing { fun Application.verify() {

    } } } data class Response(val status: String) install(ContentNegotiation) install(StatusPages) { }1 ... { }2 @JsonClass(generateAdapter = true) ...
  46. call.respond(Response(status = "OK")) post("/verify") { routing { fun Application.verify() {

    } } } data class Response(val status: String) install(ContentNegotiation) install(StatusPages) { }1 ... { }2 @JsonClass(generateAdapter = true) ... ≈ ≈ ≈ ≈ @JsonClass(generateAdapter = true) data class Request( val userId: String, val packageName: String, val productId: String, val token: String )b
  47. call.respond(Response(status = "OK")) post("/verify") { routing { fun Application.verify() {

    } } } data class Response(val status: String) install(ContentNegotiation) install(StatusPages) { }1 ... { }2 @JsonClass(generateAdapter = true) ... @JsonClass(generateAdapter = true) data class Request( val userId: String, val packageName: String, val productId: String, val token: String )b ≈ ≈ ≈ bird
  48. @JsonClass(generateAdapter = true) data class Request( val userId: String, val

    packageName: String, val productId: String, val token: String )b post("/verify") { routing { fun Application.verify() { } } } data class Response(val status: String) install(ContentNegotiation) install(StatusPages) { }1 ... { }2 @JsonClass(generateAdapter = true) ... ≈ ≈ ≈ bird
  49. @JsonClass(generateAdapter = true) data class Request( val userId: String, val

    packageName: String, val productId: String, val token: String )b post("/verify") { routing { fun Application.verify() { } } } data class Response(val status: String) install(ContentNegotiation) install(StatusPages) { }1 ... { }2 @JsonClass(generateAdapter = true) ... ≈ ≈ ≈ val request = call.receive<Request>() ≈ ≈ bird
  50. @JsonClass(generateAdapter = true) data class Request( val userId: String, val

    packageName: String, val productId: String, val token: String )b post("/verify") { routing { fun Application.verify() { } } } data class Response(val status: String) install(ContentNegotiation) install(StatusPages) { }1 ... { }2 @JsonClass(generateAdapter = true) ... ≈ ≈ ≈ val request = call.receive<Request>() ≈ ≈ call.respond(request) bird
  51. @JsonClass(generateAdapter = true) data class Request( val userId: String, val

    packageName: String, val productId: String, val token: String )b post("/verify") { routing { fun Application.verify() { } } } data class Response(val status: String) install(ContentNegotiation) install(StatusPages) { }1 ... { }2 @JsonClass(generateAdapter = true) ... val request = call.receive<Request>() call.respond(request)
  52. → cat << EOF >> /tmp/request.json

  53. → cat << EOF >> /tmp/request.json > { > "userId":

    "rharter", > "packageName": "com.pixite.pigment", > "productId": "com.pixite.pigment.subscription.monthly_t", > "token": “fpljlfogiejllhkebmjkpndm.AO-Oy5r83Kzef5afyMfL0suZM11l76cp_WdnWgOz... > } > EOF →
  54. → curl -X POST —H "Content-Type: application/json" -d @/tmp/request.json \

    http://localhost:8080/verify
  55. → curl -X POST -H "Content-Type: application/json" -d @/tmp/request.json \

    http://localhost:8080/verify {“userId”:”rharter","packageName":"com.pixite.pigment","productId":"com.pixite.pig ment.subscription.monthly_t","token":"fpljlfogiejllhkebmjkpndm.AO- J1Oy5r83Kzef5afyMfL0suZM11l76cp_WdnWgOz..."}
  56. @JsonClass(generateAdapter = true) data class Request( val userId: String, val

    packageName: String, val productId: String, val token: String )b post("/verify") { routing { fun Application.verify() { } } } data class Response(val status: String) install(ContentNegotiation) install(StatusPages) { }1 ... { }2 @JsonClass(generateAdapter = true) ... val request = call.receive<Request>() call.respond(request)
  57. @JsonClass(generateAdapter = true) data class Request( val userId: String, val

    packageName: String, val productId: String, val token: String )b post("/verify") { routing { fun Application.verify() { } } } install(ContentNegotiation) install(StatusPages) { }1 ... { }2 ... val request = call.receive<Request>() call.respond(request) ???
  58. External Components

  59. @JsonClass(generateAdapter = true) data class Request( val userId: String, val

    packageName: String, val productId: String, val token: String )b post("/verify") { routing { fun Application.verify() { } } } install(ContentNegotiation) install(StatusPages) { }1 ... { }2 ... val request = call.receive<Request>() call.respond(request) ???
  60. @JsonClass(generateAdapter = true) data class Request( val userId: String, val

    packageName: String, val productId: String, val token: String )b post("/verify") { routing { fun Application.verify() { } } } install(ContentNegotiation) install(StatusPages) { }1 ... { }2 ... val request = call.receive<Request>() call.respond(request) // Find valid subscription in db or remotely ≈ ≈
  61. @JsonClass(generateAdapter = true) data class Request( val userId: String, val

    packageName: String, val productId: String, val token: String )b post("/verify") { routing { fun Application.verify() { } } } install(ContentNegotiation) install(StatusPages) { }1 ... { }2 ... val request = call.receive<Request>() // Find valid subscription in db or remotely // Save to database ≈ ≈
  62. // Return subscription or 404 @JsonClass(generateAdapter = true) data class

    Request( val userId: String, val packageName: String, val productId: String, val token: String )b post("/verify") { routing { fun Application.verify() { } } } install(ContentNegotiation) install(StatusPages) { }1 ... { }2 ... val request = call.receive<Request>() // Find valid subscription in db or remotely // Save to database ≈ ≈
  63. interface Api { suspend fun findSubscription(userId: String, packageName: String, productId:

    String, token: String): Subscription? }
  64. class PlayStore(serviceAccountFile: InputStream) : Store { private val publisher: AndroidPublisher

    by lazy {...} suspend fun findSubscription(userId: String, packageName: String, productId: String, token: String): Subscription? = coroutineScope { val response = async { publisher.purchases() .subscriptions() .get(packageName, productId, token) .execute() } response.await().asSubscription(ownerId, token) } }
  65. class PlayStore(serviceAccountFile: InputStream) : Store { private val publisher: AndroidPublisher

    by lazy {...} suspend fun findSubscription(userId: String, packageName: String, productId: String, token: String): Subscription? = coroutineScope { val response = async { publisher.purchases() .subscriptions() .get(packageName, productId, token) .execute() } response.await().asSubscription(ownerId, token) } } class top class bottom
  66. class PlayStore(serviceAccountFile: InputStream) : Store { private val publisher: AndroidPublisher

    by lazy {...} suspend fun findSubscription(userId: String, packageName: String, productId: String, token: String): Subscription? = coroutineScope { val response = async { publisher.purchases() .subscriptions() .get(packageName, productId, token) .execute() } response.await().asSubscription(ownerId, token) } } class top class bottom
  67. interface Database { suspend fun subscription(subscriptionId: String): Subscription? suspend fun

    subscriptionByToken(token: String): Subscription? suspend fun subscriptionByUserId(userId: String): Subscription? suspend fun createSubscription(subscription: Subscription): Subscription }
  68. // Return subscription or 404 @JsonClass(generateAdapter = true) data class

    Request( val userId: String, val packageName: String, val productId: String, val token: String )b post("/verify") { routing { fun Application.verify() { } } } install(ContentNegotiation) install(StatusPages) { }1 ... { }2 ... val request = call.receive<Request>() // Find valid subscription in db or remotely // Save to database
  69. // Return subscription or 404 @JsonClass(generateAdapter = true) data class

    Request( val userId: String, val packageName: String, post("/verify") { routing { fun Application.verify() { } } } install(ContentNegotiation) install(StatusPages) { }1 ... { }2 ... val request = call.receive<Request>() // Find valid subscription in db or remotely // Save to database ≈ ≈ ≈ val api = TotallyRealApi() ≈ ≈ ≈
  70. // Return subscription or 404 @JsonClass(generateAdapter = true) data class

    Request( val userId: String, post("/verify") { routing { fun Application.verify() { } } } install(ContentNegotiation) install(StatusPages) { }1 ... { }2 ... val request = call.receive<Request>() // Find valid subscription in db or remotely // Save to database ≈ ≈ ≈ val db = InMemoryDatabase() val api = TotallyRealApi() ≈ ≈ ≈
  71. // Return subscription or 404 @JsonClass(generateAdapter = true) data class

    Request( val userId: String, post("/verify") { routing { fun Application.verify() { } } } install(ContentNegotiation) install(StatusPages) { }1 ... { }2 ... val request = call.receive<Request>() // Save to database ≈ ≈ ≈ val db = InMemoryDatabase() val api = TotallyRealApi() ≈ ≈ val subscription = db.subscriptionByUserId(request.userId)
  72. // Return subscription or 404 @JsonClass(generateAdapter = true) data class

    Request( post("/verify") { routing { fun Application.verify() { } } } install(ContentNegotiation) install(StatusPages) { }1 ... { }2 ... val request = call.receive<Request>() ≈ ≈ val db = InMemoryDatabase() val api = TotallyRealApi() ≈ ?: api.findSubscription(request.userId, request.packageName request.productId, request.token) val subscription = db.subscriptionByUserId(request.userId)
  73. ?: api.findSubscription(request.userId, request.packageName request.productId, request.token) // Return subscription or 404

    @JsonClass(generateAdapter = true) data class Request( post("/verify") { routing { fun Application.verify() { } } } install(ContentNegotiation) install(StatusPages) { }1 ... { }2 ... val request = call.receive<Request>() ≈ ≈ val db = InMemoryDatabase() val api = TotallyRealApi() ≈ ?.also { db.createSubscription(it) } val subscription = db.subscriptionByUserId(request.userId)
  74. post("/verify") { routing { fun Application.verify() { } } install(ContentNegotiation)

    install(StatusPages) { }1 ... { }2 ... val request = call.receive<Request>() ≈ ≈ val db = InMemoryDatabase() val api = TotallyRealApi() val subscription = db.subscriptionByUserId(request.userId) if (subscription == null) { call.respond(HttpStatusCode.NotFound, "Subscription invalid.") } else { call.respond(subscription) } ?: api.findSubscription(request.userId, request.packageName request.productId, request.token) ?.also { db.createSubscription(it) }
  75. → curl -X POST -H "Content-Type: application/json" -d @valid.json http://localhost:8080/verify

  76. → curl -X POST -H "Content-Type: application/json" -d @valid.json http://localhost:8080/verify

    {“canceled":false,"expiryDate":"2018-10-12T04:35:21.000Z","id":"668245e4-b673-4258- ba7c-4b0367833e61","ownerId":"rharter","startDate":"2018-09-12T04:35:21.000Z","token":"token3"}
  77. → curl -X POST -H "Content-Type: application/json" -d @valid.json http://localhost:8080/verify

    {“canceled":false,"expiryDate":"2018-10-12T04:35:21.000Z","id":"668245e4-b673-4258- ba7c-4b0367833e61","ownerId":"rharter","startDate":"2018-09-12T04:35:21.000Z","token":"token3"} → → curl -X POST -H "Content-Type: application/json" -d @invalid.json http://localhost:8080/verify
  78. → curl -X POST -H "Content-Type: application/json" -d @valid.json http://localhost:8080/verify

    {“canceled":false,"expiryDate":"2018-10-12T04:35:21.000Z","id":"668245e4-b673-4258- ba7c-4b0367833e61","ownerId":"rharter","startDate":"2018-09-12T04:35:21.000Z","token":"token3"} → → curl -X POST -H "Content-Type: application/json" -d @invalid.json http://localhost:8080/verify Subscription invalid. →
  79. Templates

  80. post("/verify") { routing { fun Application.verify() { } } install(ContentNegotiation)

    install(StatusPages) { }1 ... { }2 ... val request = call.receive<Request>() val db = InMemoryDatabase() val api = TotallyRealApi() val subscription = db.subscriptionByUserId(request.userId) if (subscription == null) { call.respond(HttpStatusCode.NotFound, "Subscription invalid.") } else { call.respond(subscription) } ?: api.findSubscription(request.userId, request.packageName request.productId, request.token) ?.also { db.createSubscription(it) }
  81. post("/verify") { routing { fun Application.verify() { } } install(ContentNegotiation)

    install(StatusPages) { }1 ... { }2 ... ... }3 ...
  82. get(“/subscriptions") { routing { fun Application.verify() { } } install(ContentNegotiation)

    install(StatusPages) { }1 ... { }2 ... }4 ... post("/verify") {... }3
  83. get(“/subscriptions") { routing { fun Application.verify() { } } install(ContentNegotiation)

    install(StatusPages) { }1 ... { }2 ... }4 ... post("/verify") {... }3 val subscriptions = db.subscriptions() call.respond(subscriptions)
  84. http://localhost:8080/subscriptions [ { "id": "sub-1", "ownerId": "Bandalls", "token": "asdf98hn", "startDate":

    "Sep 11, 2018 11:35:21 PM", "expiryDate": "Oct 11, 2018 11:35:21 PM", "canceled": true }, { "id": "sub-2", "ownerId": "Editussion", "token": "asdf092s", "startDate": "Sep 11, 2018 11:35:21 PM", "expiryDate": "Oct 11, 2018 11:35:21 PM", "canceled": false }, { "id": "sub-3", "ownerId": "Liveltekah", "token": "gju0u0fe", "startDate": "Sep 11, 2018 11:35:21 PM", "expiryDate": "Oct 11, 2018 11:35:21 PM", "canceled": false }, { "id": "sub-4", "ownerId": "Ortspoon", "token": "diiefh48", "startDate": "Sep 11, 2018 11:35:21 PM", "expiryDate": "Oct 11, 2018 11:35:21 PM", "canceled": true }, { "id": "sub-5", "ownerId": "Reakefit", "token": "dg09uui2", http://localhost:8080/subs…
  85. get(“/subscriptions") { routing { fun Application.verify() { } } install(ContentNegotiation)

    install(StatusPages) { }1 ... { }2 ... }4 ... post("/verify") {... }3 val subscriptions = db.subscriptions() call.respond(subscriptions)
  86. install(FreeMarker) { get(“/subscriptions") { routing { fun Application.verify() { }

    } install(ContentNegotiation) install(StatusPages) { }1 ... { }2 ... }4 ... post("/verify") {... }3 val subscriptions = db.subscriptions() call.respond(subscriptions) }freemarker
  87. install(FreeMarker) { get(“/subscriptions") { routing { fun Application.verify() { }

    } install(ContentNegotiation) install(StatusPages) { }1 ... { }2 ... }4 ... post("/verify") {... }3 val subscriptions = db.subscriptions() call.respond(subscriptions) }freemarker templateLoader = ClassTemplateLoader( this@module.javaClass.classLoader, "templates" )
  88. install(FreeMarker) { get(“/subscriptions") { routing { fun Application.verify() { }

    } install(ContentNegotiation) install(StatusPages) { }1 ... { }2 ... }4 ... post("/verify") {... }3 val subscriptions = db.subscriptions() call.respond(subscriptions) }freemarker ...
  89. install(FreeMarker) { get(“/subscriptions") { routing { fun Application.verify() { }

    } install(ContentNegotiation) install(StatusPages) { }1 ... { }2 ... }4 ... post("/verify") {... }3 val subscriptions = db.subscriptions() call.respond( }freemarker ... subscriptions)respond
  90. install(FreeMarker) { get(“/subscriptions") { routing { fun Application.verify() { }

    } install(ContentNegotiation) install(StatusPages) { }1 ... { }2 ... }4 ... post("/verify") {... }3 val subscriptions = db.subscriptions() call.respond( }freemarker ... )respond FreeMarkerContent("subscriptions.ftl", mapOf("subscriptions" to )) subscriptions
  91. install(FreeMarker) { get(“/subscriptions") { routing { fun Application.verify() { install(ContentNegotiation)

    install(StatusPages) { }1 ... { }2 ... ... val subscriptions = db.subscriptions() call.respond( }freemarker ... FreeMarkerContent("subscriptions.ftl", mapOf("subscriptions" to )) subscriptions } } }4 post("/verify") {... }3 )respond
  92. install(FreeMarker) { get(“/subscriptions") { routing { fun Application.verify() { install(ContentNegotiation)

    install(StatusPages) { }1 ... { }2 ... ... val subscriptions = db.subscriptions() call.respond( }freemarker ... FreeMarkerContent("subscriptions.ftl", mapOf("subscriptions" to )) subscriptions } } }4 post("/verify") {... }3 )respond
  93. <head> <link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons"> <link rel="stylesheet" href="https://code.getmdl.io/1.3.0/material.indigo-pink.min.css"> <script defer src="https://code.getmdl.io/1.3.0/material.min.js"></script>

    </head> <body> <div class="demo-layout mdl-layout mdl-js-layout mdl-layout--fixed-header"> <header class="demo-header mdl-layout__header mdl-color--grey-100 mdl-color-text--grey-600"> <div class="mdl-layout__header-row"> <span class="mdl-layout-title">Home</span> <div class="mdl-layout-spacer"></div> <div class="mdl-textfield mdl-js-textfield mdl-textfield--expandable"> <label class="mdl-button mdl-js-button mdl-button--icon" for="search"> <i class="material-icons">search</i> </label> <div class="mdl-textfield__expandable-holder"> <input class="mdl-textfield__input" type="text" id="search"> <label class="mdl-textfield__label" for="search">Enter your query...</label> </div> </div> <button class="mdl-button mdl-js-button mdl-js-ripple-effect mdl-button--icon" id="hdrbtn"> <i class="material-icons">more_vert</i> </button> <ul class="mdl-menu mdl-js-menu mdl-js-ripple-effect mdl-menu--bottom-right" for="hdrbtn"> <li class="mdl-menu__item">About</li> <li class="mdl-menu__item">Contact</li> <li class="mdl-menu__item">Legal information</li> </ul> </div> </header> <main class="mdl-layout__content mdl-color--grey-100"> <div class="mdl-grid"> <table class="mdl-data-table mdl-js-data-table mdl-data-table--selectable mdl-color--white mdl-shadow--2dp mdl-cell mdl-cell--12-col"> <thead> <tr> <th class="mdl-data-table__cell--non-numeric">Owner ID</th> <th class="mdl-data-table__cell--non-numeric">Start Date</th> <th class="mdl-data-table__cell--non-numeric">Expiry Date</th> <th class="mdl-data-table__cell--non-numeric">Cancelled</th> </tr> </thead> <tbody> <#list subscriptions as subscription> <tr> <td class="mdl-data-table__cell--non-numeric">${subscription.ownerId}</td> <td class="mdl-data-table__cell--non-numeric">${subscription.startDate?date}</td> <td class="mdl-data-table__cell--non-numeric">${subscription.expiryDate?date}</td> <td class="mdl-data-table__cell--non-numeric"> ${subscription.canceled?string('yes', 'no')} </td> </tr> </#list> </tbody> </table> </div> </main> </div>
  94. <th class="mdl-data-table__cell--non-numeric">Start Date</th> <th class="mdl-data-table__cell--non-numeric">Expiry Date</th> <th class="mdl-data-table__cell--non-numeric">Cancelled</th> </tr> </thead>

    <tbody> <#list subscriptions as subscription> <tr> <td class="mdl-data-table__cell--non-numeric">${subscription.ownerId}</td> <td class="mdl-data-table__cell--non-numeric">${subscription.startDate?date}</td> <td class="mdl-data-table__cell--non-numeric">${subscription.expiryDate?date}</td> <td class="mdl-data-table__cell--non-numeric"> ${subscription.canceled?string('yes', 'no')} </td> </tr> </#list> </tbody> /table> >
  95. [ { "id": "sub-1", "ownerId": "Bandalls", "token": "asdf98hn", "startDate": "Sep

    11, 2018 11:35:21 PM", "expiryDate": "Oct 11, 2018 11:35:21 PM", "canceled": true }, { "id": "sub-2", "ownerId": "Editussion", "token": "asdf092s", "startDate": "Sep 11, 2018 11:35:21 PM", "expiryDate": "Oct 11, 2018 11:35:21 PM", "canceled": false }, { "id": "sub-3", "ownerId": "Liveltekah", "token": "gju0u0fe", "startDate": "Sep 11, 2018 11:35:21 PM", "expiryDate": "Oct 11, 2018 11:35:21 PM", "canceled": false }, { "id": "sub-4", "ownerId": "Ortspoon", "token": "diiefh48", "startDate": "Sep 11, 2018 11:35:21 PM", "expiryDate": "Oct 11, 2018 11:35:21 PM", "canceled": true }, { "id": "sub-5", "ownerId": "Reakefit", "token": "dg09uui2", http://localhost:8080/subscriptions http://localhost:8080/subs…
  96. [ { "id": "sub-1", "ownerId": "Bandalls", "token": "asdf98hn", "startDate": "Sep

    11, 2018 11:35:21 PM", "expiryDate": "Oct 11, 2018 11:35:21 PM", "canceled": true }, { "id": "sub-2", "ownerId": "Editussion", "token": "asdf092s", "startDate": "Sep 11, 2018 11:35:21 PM", "expiryDate": "Oct 11, 2018 11:35:21 PM", "canceled": false }, { "id": "sub-3", "ownerId": "Liveltekah", "token": "gju0u0fe", "startDate": "Sep 11, 2018 11:35:21 PM", "expiryDate": "Oct 11, 2018 11:35:21 PM", "canceled": false }, { "id": "sub-4", "ownerId": "Ortspoon", "token": "diiefh48", "startDate": "Sep 11, 2018 11:35:21 PM", "expiryDate": "Oct 11, 2018 11:35:21 PM", "canceled": true }, { "id": "sub-5", "ownerId": "Reakefit", "token": "dg09uui2", http://localhost:8080/subscriptions http://localhost:8080/subs…
  97. http://localhost:8080/subscriptions http://localhost:8080/subs…

  98. Authentication

  99. }routing install(FreeMarker) { get("/subscriptions") { routing { fun Application.verify() {

    install(ContentNegotiation) install(StatusPages) { }1 ... { }2 ... ... val subscriptions = db.subscriptions() call.respond( }freemarker ... FreeMarkerContent("subscriptions.ftl", mapOf("subscriptions" to )) subscriptions } }4 post("/verify") {... }3 )respond
  100. }routing install(FreeMarker) { get("/subscriptions") { routing { fun Application.verify() {

    install(ContentNegotiation) install(StatusPages) { }1 ... { }2 ... }freemarker ... } }4 post("/verify") {... }3 ...
  101. }routing install(FreeMarker) { get("/subscriptions") { routing { fun Application.verify() {

    install(ContentNegotiation) install(StatusPages) { }1 ... { }2 ... }freemarker ... } }4 post("/verify") {... }3 ... install(Authentication) s
  102. install(FreeMarker) { get("/subscriptions") { routing { fun Application.verify() { install(ContentNegotiation)

    install(StatusPages) { }1 ... { }2 ... }freemarker ... } }4 post("/verify") {... }3 ... install(Authentication) {auth }auth }basic basic(name = "userAuth") {basic }routing s
  103. install(FreeMarker) { get("/subscriptions") { routing { fun Application.verify() { install(ContentNegotiation)

    install(StatusPages) { }1 ... { }2 ... }freemarker ... } }4 post("/verify") {... }3 ... install(Authentication) {auth }auth }basic realm = "Verifier" basic(name = "userAuth") {basic }routing s
  104. }routing install(FreeMarker) { get("/subscriptions") { routing { fun Application.verify() {

    install(ContentNegotiation) install(StatusPages) { }1 ... { }2 ... }freemarker ... } }4 post("/verify") {... }3 ... install(Authentication) s {auth }auth }validate }basic validate { credentials -> realm = "Verifier" basic(name = "userAuth") {basic
  105. }routing install(FreeMarker) { get("/subscriptions") { routing { fun Application.verify() {

    install(ContentNegotiation) install(StatusPages) { }1 ... { }2 ... }freemarker ... } }4 post("/verify") {... }3 ... install(Authentication) s {auth }auth authService.authenticate(credentials.name, credentials.password) }validate }basic validate { credentials -> realm = "Verifier" basic(name = "userAuth") {basic
  106. }routing install(FreeMarker) { get("/subscriptions") { routing { fun Application.verify() {

    install(ContentNegotiation) install(StatusPages) { }1 ... { }2 ... }freemarker ... } }4 post("/verify") {... }3 ... install(Authentication) s {auth }auth ...
  107. }routing install(FreeMarker) { get("/subscriptions") { routing { fun Application.verify() {

    install(ContentNegotiation) install(StatusPages) { }1 ... { }2 ... }freemarker ... } }4 post("/verify") {... }3 ... install(Authentication) {auth }auth ...
  108. get("/subscriptions") { fun Application.verify() { } } }4 post("/verify") {...

    }3 ... authenticate("userAuth") { } install(FreeMarker) { routing { install(ContentNegotiation) install(StatusPages) { }1 ... { }2 ... }freemarker ... install(Authentication) {auth }auth ...
  109. fun Application.verify() { get("/subscriptions") { } } }4 post("/verify") {...

    }3 ... authenticate("userAuth") { } install(FreeMarker) { routing { install(ContentNegotiation) install(StatusPages) { }1 ... { }2 ... }freemarker ... install(Authentication) {auth }auth ...
  110. fun Application.verify() { } } post("/verify") {... }3 authenticate("userAuth") {

    } get("/subscriptions") { val subscriptions = db.subscriptions() call.respond( FreeMarkerContent("subscriptions.ftl", mapOf("subscriptions" to subscriptions }4 )respond )) install(FreeMarker) { routing { install(ContentNegotiation) install(StatusPages) { }1 ... { }2 ... }freemarker ... install(Authentication) {auth }auth ...
  111. fun Application.verify() { } } post("/verify") {... }3 authenticate("userAuth") {

    } get("/subscriptions") { val subscriptions = db.subscriptions() call.respond( FreeMarkerContent("subscriptions.ftl", mapOf( subscriptions }4 )respond val user = call.authentication.principal d "subscriptions" to )) install(FreeMarker) { routing { install(ContentNegotiation) install(StatusPages) { }1 ... { }2 ... }freemarker ... install(Authentication) {auth }auth ...
  112. } val subscriptions = db.subscriptions() call.respond( FreeMarkerContent("subscriptions.ftl", mapOf( subscriptions }4

    )respond val user = call.authentication.principal d ) "subscriptions" to ) , "user" to user fun Application.verify() { authenticate("userAuth") { get("/subscriptions") { install(FreeMarker) { routing { install(ContentNegotiation) install(StatusPages) { }1 ... { }2 ... }freemarker ... install(Authentication) {auth }auth ...
  113. [ { "id": "sub-1", "ownerId": "Bandalls", "token": "asdf98hn", "startDate": "Sep

    11, 2018 11:35:21 PM", "expiryDate": "Oct 11, 2018 11:35:21 PM", "canceled": true }, { "id": "sub-2", "ownerId": "Editussion", "token": "asdf092s", "startDate": "Sep 11, 2018 11:35:21 PM", "expiryDate": "Oct 11, 2018 11:35:21 PM", "canceled": false }, { "id": "sub-3", "ownerId": "Liveltekah", "token": "gju0u0fe", "startDate": "Sep 11, 2018 11:35:21 PM", "expiryDate": "Oct 11, 2018 11:35:21 PM", "canceled": false }, { "id": "sub-4", "ownerId": "Ortspoon", "token": "diiefh48", "startDate": "Sep 11, 2018 11:35:21 PM", "expiryDate": "Oct 11, 2018 11:35:21 PM", "canceled": true }, { "id": "sub-5", "ownerId": "Reakefit", "token": "dg09uui2", http://localhost:8080/subscriptions http://localhost:8080/subs…
  114. http://localhost:8080/subscriptions http://localhost:8080/subs… Sign in http://localhost:8080 Username Password Sign in Cancel

  115. http://localhost:8080/subscriptions http://localhost:8080/subs… Sign in http://localhost:8080 Username Password Sign in Cancel

    admin •••••••••••
  116. http://localhost:8080/subscriptions http://localhost:8080/subs… Ryan

  117. Forms

  118. http://localhost:8080/subscriptions http://localhost:8080/subs… Ryan

  119. http://localhost:8080/subscriptions http://localhost:8080/subs… Ryan

  120. Ryan

  121. } } post("/verify") {... }3 } }4 d fun Application.verify()

    { authenticate("userAuth") { get("/subscriptions") { install(FreeMarker) { routing { install(ContentNegotiation) install(StatusPages) { }1 ... { }2 ... }freemarker ... install(Authentication) {auth }auth ... ...
  122. post("/verify") {... }3 } }4 authenticate("userAuth") { get("/subscriptions") { install(FreeMarker)

    { routing { }freemarker ... install(Authentication) {auth }auth ... ... post("/subscriptions") { }post get("/subscriptions") {
  123. post("/verify") {... }3 } }4 authenticate("userAuth") { get("/subscriptions") { install(FreeMarker)

    { routing { }freemarker ... install(Authentication) {auth }auth ... ... post("/subscriptions") { }post get("/subscriptions") { val parameters = call.receiveParameters()
  124. post("/verify") {... }3 } }4 authenticate("userAuth") { get("/subscriptions") { install(FreeMarker)

    { routing { }freemarker ... install(Authentication) {auth }auth ... ... post("/subscriptions") { }post get("/subscriptions") { val parameters = call.receiveParameters() val id = parameters[“id"] post("/subscriptions") { end post
  125. post("/verify") {... }3 } }4 authenticate("userAuth") { get("/subscriptions") { install(FreeMarker)

    { routing { }freemarker ... install(Authentication) {auth }auth ... ... post("/subscriptions") { }post get("/subscriptions") { val parameters = call.receiveParameters() val id = parameters[“id"] ?: throw IllegalArgumentException("Missing parameter: id") post("/subscriptions") { end post
  126. post("/verify") {... }3 } }4 authenticate("userAuth") { get("/subscriptions") { install(FreeMarker)

    { routing { }freemarker ... install(Authentication) {auth }auth ... ... post("/subscriptions") { }post get("/subscriptions") { val parameters = call.receiveParameters() val id = parameters[“id"] ?: throw IllegalArgumentException("Missing parameter: id") val action = parameters[“action"] ?: throw IllegalArgumentException("Missing parameter: action") post("/subscriptions") { end post
  127. post("/verify") {... }3 } }4 authenticate("userAuth") { get("/subscriptions") { install(FreeMarker)

    { routing { }freemarker ... install(Authentication) {auth }auth ... ... post("/subscriptions") { }post get("/subscriptions") { val parameters = call.receiveParameters() val id = parameters[“id"] ?: throw IllegalArgumentException("Missing parameter: id") val action = parameters[“action"] ?: throw IllegalArgumentException("Missing parameter: action") when (action) { }when post("/subscriptions") { val parameters = call.receiveParameters() ids and such end post
  128. post("/verify") {... }3 } }4 authenticate("userAuth") { get("/subscriptions") { install(FreeMarker)

    { routing { }freemarker ... install(Authentication) {auth }auth ... ... post("/subscriptions") { }post get("/subscriptions") { val parameters = call.receiveParameters() val id = parameters[“id"] ?: throw IllegalArgumentException("Missing parameter: id") val action = parameters[“action"] ?: throw IllegalArgumentException("Missing parameter: action") when (action) { }when "delete" -> db.deleteSubscription(id) post("/subscriptions") { val parameters = call.receiveParameters() ids and such end post when (action) { end when
  129. post("/verify") {... }3 } }4 authenticate("userAuth") { get("/subscriptions") { install(FreeMarker)

    { routing { }freemarker ... install(Authentication) {auth }auth ... ... post("/subscriptions") { }post get("/subscriptions") { val parameters = call.receiveParameters() val id = parameters[“id"] ?: throw IllegalArgumentException("Missing parameter: id") val action = parameters[“action"] ?: throw IllegalArgumentException("Missing parameter: action") when (action) { }when "delete" -> db.deleteSubscription(id) "cancel" -> { } -> cancel post("/subscriptions") { val parameters = call.receiveParameters() ids and such end post when (action) { “delete” -> db.deleteSubscription(id) end when
  130. post("/verify") {... }3 } }4 authenticate("userAuth") { get("/subscriptions") { install(FreeMarker)

    { routing { }freemarker ... install(Authentication) {auth }auth ... ... post("/subscriptions") { }post get("/subscriptions") { val parameters = call.receiveParameters() val id = parameters[“id"] ?: throw IllegalArgumentException("Missing parameter: id") val action = parameters[“action"] ?: throw IllegalArgumentException("Missing parameter: action") when (action) { }when "delete" -> db.deleteSubscription(id) "cancel" -> { db.subscription(id) } -> cancel post("/subscriptions") { val parameters = call.receiveParameters() ids and such end post when (action) { “delete” -> db.deleteSubscription(id) end when
  131. post("/verify") {... }3 } }4 authenticate("userAuth") { get("/subscriptions") { install(FreeMarker)

    { routing { }freemarker ... install(Authentication) {auth }auth ... ... post("/subscriptions") { }post get("/subscriptions") { val parameters = call.receiveParameters() val id = parameters[“id"] ?: throw IllegalArgumentException("Missing parameter: id") val action = parameters[“action"] ?: throw IllegalArgumentException("Missing parameter: action") when (action) { }when "delete" -> db.deleteSubscription(id) "cancel" -> { db.subscription(id) db.putSubscription(it.copy(canceled = true)) }db.subscription(id)?.also } -> cancel ?.also { post("/subscriptions") { val parameters = call.receiveParameters() ids and such end post when (action) { “delete” -> db.deleteSubscription(id) end when
  132. post("/verify") {... }3 } }4 authenticate("userAuth") { get("/subscriptions") { install(FreeMarker)

    { routing { }freemarker ... install(Authentication) {auth }auth ... ... post("/subscriptions") { val parameters = call.receiveParameters() val id = parameters[“id"] ?: throw IllegalArgumentException("Missing parameter: id") val action = parameters[“action"] ?: throw IllegalArgumentException("Missing parameter: action") when (action) { "delete" -> db.deleteSubscription(id) "cancel" -> { db.subscription(id)?.also { db.putSubscription(it.copy(canceled = true)) }db.subscription(id)?.also } -> cancel }when }post call.respondRedirect("/subscriptions") get("/subscriptions") { post("/subscriptions") { val parameters = call.receiveParameters() ids and such end post when (action) { “delete” -> db.deleteSubscription(id) end when cancel stuff
  133. <th class="mdl-data-table__cell--non-numeric">Expiry Date</th> <th class="mdl-data-table__cell--non-numeric">Cancelled</th> </tr> </thead> <tbody> <#list subscriptions

    as subscription> <tr> <td class="mdl-data-table__cell--non-numeric">${subscription.ownerId}</td> <td class="mdl-data-table__cell--non-numeric">${subscription.startDate?date}</td> <td class="mdl-data-table__cell--non-numeric">${subscription.expiryDate?date}</td> <td class="mdl-data-table__cell--non-numeric"> ${subscription.canceled?string('yes', 'no')} </td> </tr> </#list> </tbody> /table> >
  134. <td class="mdl-data-table__cell--non-numeric"> ${subscription.canceled?string('yes', 'no')} </td> </tr> </#list> </tbody> <td class="mdl-data-table__cell--non-numeric">

    <form method="post" action="subscriptions"> <input type="hidden" name="id" value="${subscription.id}"/> <input type="hidden" name="action" value="cancel"/> <button type="submit" title="Cancel" class=“…” <#if subscription.canceled>disabled</#if>> <i class="material-icons">block</i> </button> </form> <form method="post" action="subscriptions"> <input type="hidden" name="id" value="${subscription.id}"/> <input type="hidden" name="action" value="delete"/> <button type="submit" title="Delete" class=“…”> <i class="material-icons">delete</i> </button> </form> </td> td /td
  135. <td class="mdl-data-table__cell--non-numeric"> ${subscription.canceled?string('yes', 'no')} </td> </tr> </#list> </tbody> <td class="mdl-data-table__cell--non-numeric">

    <form method="post" action="subscriptions"> <input type="hidden" name="id" value="${subscription.id}"/> <input type="hidden" name="action" value="cancel"/> <button type="submit" title="Cancel" class=“…” <#if subscription.canceled>disabled</#if>> <i class="material-icons">block</i> </button> </form> <form method="post" action="subscriptions"> <input type="hidden" name="id" value="${subscription.id}"/> <input type="hidden" name="action" value="delete"/> <button type="submit" title="Delete" class=“…”> <i class="material-icons">delete</i> </button> </form> </td> td /td start td End td
  136. <td class="mdl-data-table__cell--non-numeric"> ${subscription.canceled?string('yes', 'no')} </td> </tr> </#list> </tbody> <td class="mdl-data-table__cell--non-numeric">

    <form method="post" action="subscriptions"> <input type="hidden" name="id" value="${subscription.id}"/> <input type="hidden" name="action" value="cancel"/> <button type="submit" title="Cancel" class=“…” <#if subscription.canceled>disabled</#if>> <i class="material-icons">block</i> </button> </form> <form method="post" action="subscriptions"> <input type="hidden" name="id" value="${subscription.id}"/> <input type="hidden" name="action" value="delete"/> <button type="submit" title="Delete" class=“…”> <i class="material-icons">delete</i> </button> </form> </td> td /td start td End td form content
  137. <td class="mdl-data-table__cell--non-numeric"> ${subscription.canceled?string('yes', 'no')} </td> </tr> </#list> </tbody> <td class="mdl-data-table__cell--non-numeric">

    <form method="post" action="subscriptions"> <input type="hidden" name="id" value="${subscription.id}"/> <input type="hidden" name="action" value="cancel"/> <button type="submit" title="Cancel" class=“…” <#if subscription.canceled>disabled</#if>> <i class="material-icons">block</i> </button> </form> <form method="post" action="subscriptions"> <input type="hidden" name="id" value="${subscription.id}"/> <input type="hidden" name="action" value="delete"/> <button type="submit" title="Delete" class=“…”> <i class="material-icons">delete</i> </button> </form> </td> td /td start td End td formstart td /form td button
  138. <td class="mdl-data-table__cell--non-numeric"> ${subscription.canceled?string('yes', 'no')} </td> </tr> </#list> </tbody> <td class="mdl-data-table__cell--non-numeric">

    <form method="post" action="subscriptions"> <input type="hidden" name="id" value="${subscription.id}"/> <input type="hidden" name="action" value="cancel"/> <button type="submit" title="Cancel" class=“…” <#if subscription.canceled>disabled</#if>> <i class="material-icons">block</i> </button> </form> <form method="post" action="subscriptions"> <input type="hidden" name="id" value="${subscription.id}"/> <input type="hidden" name="action" value="delete"/> <button type="submit" title="Delete" class=“…”> <i class="material-icons">delete</i> </button> </form> </td> td /td start td End td formstart td /form td inputs
  139. http://localhost:8080/subscriptions http://localhost:8080/subs… Ryan

  140. http://localhost:8080/subscriptions http://localhost:8080/subs… Ryan Cancel

  141. http://localhost:8080/subscriptions http://localhost:8080/subs… Ryan

  142. Ryan

  143. Ryan

  144. Ktor • Web • Github • Slack http://ktor.io ktorio /

    ktor kotlinlang #ktor
  145. Ktor • Web • Github • Slack • Me http://ktor.io

    ktorio / ktor kotlinlang #ktor @rharter
  146. Servers ❤ Kotlin Ryan Harter @rharter