Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Servers ❤️ Kotlin

Servers ❤️ Kotlin

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

Ryan Harter

October 05, 2018
Tweet

More Decks by Ryan Harter

Other Decks in Technology

Transcript

  1. Servers ❤ Kotlin
    Ryan Harter
    @rharter

    View Slide

  2. Ktor

    View Slide

  3. Ktor
    Easy to use, fun and asynchronous.

    View Slide

  4. Ktor
    Easy to use, fun and asynchronous.
    Composable, DSL based web services in Kotlin

    View Slide

  5. Ktor
    Application

    View Slide

  6. Ktor
    Application
    Jetty
    Netty
    Tomcat
    Servlet

    View Slide

  7. Ktor
    Application
    Feature
    Feature
    Feature
    Feature
    Servlet

    View Slide

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

    View Slide

  9. Ktor

    View Slide

  10. Ktor
    Verify

    View Slide

  11. Ktor
    Verify
    Admin

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  19. Typed Responses

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  30. → curl -X POST -d '' http://localhost:8080/verify
    Cannot transform this request's content to class com.ryanharter.example.Response

    View Slide

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

    View Slide

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

    View Slide

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

    install(StatusPages) { }1
    ...

    View Slide

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

    install(StatusPages) { }1
    ...

    {
    }2

    View Slide

  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()


    View Slide

  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)

    View Slide

  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)

    View Slide

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

    View Slide

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

    View Slide

  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)

    View Slide

  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)
    ...

    View Slide

  42. Typed Requests

    View Slide

  43. View Slide

  44. View Slide

  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)
    ...

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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()


    bird

    View Slide

  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()


    call.respond(request)
    bird

    View Slide

  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()
    call.respond(request)

    View Slide


  52. View Slide

  53. → cat << EOF >> /tmp/request.json

    View Slide

  54. → cat << EOF >> /tmp/request.json
    > {
    > "userId": "rharter",
    > "packageName": "com.pixite.pigment",
    > "productId": "com.pixite.pigment.subscription.monthly_t",
    > "token": “fpljlfogiejllhkebmjkpndm.AO-Oy5r83Kzef5afyMfL0suZM11l76cp_WdnWgOz...
    > }
    > EOF

    View Slide


  55. View Slide

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

    View Slide

  57. → 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..."}

    View Slide

  58. @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()
    call.respond(request)

    View Slide

  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()
    call.respond(request)
    ???

    View Slide

  60. External Components

    View Slide

  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()
    call.respond(request)
    ???

    View Slide

  62. @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()
    call.respond(request)
    // Find valid subscription in db or remotely


    View Slide

  63. @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()
    // Find valid subscription in db or remotely
    // Save to database


    View Slide

  64. // 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()
    // Find valid subscription in db or remotely
    // Save to database


    View Slide

  65. interface Api {
    suspend fun findSubscription(userId: String,
    packageName: String,
    productId: String,
    token: String): Subscription?
    }

    View Slide

  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)
    }
    }

    View Slide

  67. 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

    View Slide

  68. 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

    View Slide

  69. 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
    }

    View Slide

  70. // 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()
    // Find valid subscription in db or remotely
    // Save to database

    View Slide

  71. // 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()
    // Find valid subscription in db or remotely
    // Save to database



    val api = TotallyRealApi()



    View Slide

  72. // 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()
    // Find valid subscription in db or remotely
    // Save to database



    val db = InMemoryDatabase()
    val api = TotallyRealApi()



    View Slide

  73. // 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()
    // Save to database



    val db = InMemoryDatabase()
    val api = TotallyRealApi()


    val subscription = db.subscriptionByUserId(request.userId)

    View Slide

  74. // 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()


    val db = InMemoryDatabase()
    val api = TotallyRealApi()

    ?: api.findSubscription(request.userId, request.packageName
    request.productId, request.token)
    val subscription = db.subscriptionByUserId(request.userId)

    View Slide

  75. ?: 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()


    val db = InMemoryDatabase()
    val api = TotallyRealApi()

    ?.also { db.createSubscription(it) }
    val subscription = db.subscriptionByUserId(request.userId)

    View Slide

  76. post("/verify") {
    routing {
    fun Application.verify() {
    }
    }
    install(ContentNegotiation)
    install(StatusPages) { }1
    ...
    { }2
    ...
    val request = call.receive()


    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) }

    View Slide

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

    View Slide

  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"}

    View Slide

  79. → 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

    View Slide

  80. → 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.

    View Slide

  81. Templates

    View Slide

  82. post("/verify") {
    routing {
    fun Application.verify() {
    }
    }
    install(ContentNegotiation)
    install(StatusPages) { }1
    ...
    { }2
    ...
    val request = call.receive()
    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) }

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  86. 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…

    View Slide

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

    View Slide

  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

    View Slide

  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(subscriptions)
    }freemarker
    templateLoader = ClassTemplateLoader(
    [email protected],
    "templates"
    )

    View Slide

  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(subscriptions)
    }freemarker
    ...

    View Slide

  91. 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

    View Slide

  92. 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

    View Slide

  93. 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

    View Slide

  94. 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

    View Slide










  95. Home



    search



    Enter your query...



    more_vert


    About
    Contact
    Legal information








    Owner ID
    Start Date
    Expiry Date
    Cancelled



    <#list subscriptions as subscription>

    ${subscription.ownerId}
    ${subscription.startDate?date}
    ${subscription.expiryDate?date}

    ${subscription.canceled?string('yes', 'no')}


    #list>





    View Slide

  96. Start Date
    Expiry Date
    Cancelled



    <#list subscriptions as subscription>

    ${subscription.ownerId}
    ${subscription.startDate?date}
    ${subscription.expiryDate?date}

    ${subscription.canceled?string('yes', 'no')}


    #list>

    /table>
    >

    View Slide

  97. [
    {
    "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…

    View Slide

  98. [
    {
    "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…

    View Slide

  99. http://localhost:8080/subscriptions
    http://localhost:8080/subs…

    View Slide

  100. Authentication

    View Slide

  101. }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

    View Slide

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

    View Slide

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

    View Slide

  104. 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

    View Slide

  105. 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

    View Slide

  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
    }validate
    }basic
    validate { credentials ->
    realm = "Verifier"
    basic(name = "userAuth") {basic

    View Slide

  107. }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

    View Slide

  108. }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
    ...

    View Slide

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

    View Slide

  110. 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
    ...

    View Slide

  111. 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
    ...

    View Slide

  112. 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
    ...

    View Slide

  113. 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
    ...

    View Slide

  114. }
    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
    ...

    View Slide

  115. [
    {
    "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…

    View Slide

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

    View Slide

  117. http://localhost:8080/subscriptions
    http://localhost:8080/subs…
    Sign in
    http://localhost:8080
    Username
    Password
    Sign in
    Cancel
    admin
    •••••••••••

    View Slide

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

    View Slide

  119. Forms

    View Slide

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

    View Slide

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

    View Slide

  122. Ryan

    View Slide

  123. }
    }
    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
    ...
    ...

    View Slide

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

    View Slide

  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()

    View Slide

  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"]
    post("/subscriptions") {
    end post

    View Slide

  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")
    post("/subscriptions") {
    end post

    View Slide

  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")
    post("/subscriptions") {
    end post

    View Slide

  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
    post("/subscriptions") {
    val parameters = call.receiveParameters()
    ids and such
    end post

    View Slide

  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)
    post("/subscriptions") {
    val parameters = call.receiveParameters()
    ids and such
    end post
    when (action) {
    end when

    View Slide

  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" -> {
    } -> cancel
    post("/subscriptions") {
    val parameters = call.receiveParameters()
    ids and such
    end post
    when (action) {
    “delete” -> db.deleteSubscription(id)
    end when

    View Slide

  132. 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

    View Slide

  133. 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

    View Slide

  134. 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

    View Slide

  135. Expiry Date
    Cancelled



    <#list subscriptions as subscription>

    ${subscription.ownerId}
    ${subscription.startDate?date}
    ${subscription.expiryDate?date}

    ${subscription.canceled?string('yes', 'no')}


    #list>

    /table>
    >

    View Slide


  136. ${subscription.canceled?string('yes', 'no')}


    #list>





    <#if subscription.canceled>disabled#if>>
    block






    delete



    td
    /td

    View Slide


  137. ${subscription.canceled?string('yes', 'no')}


    #list>





    <#if subscription.canceled>disabled#if>>
    block






    delete



    td
    /td
    start td
    End td

    View Slide


  138. ${subscription.canceled?string('yes', 'no')}


    #list>





    <#if subscription.canceled>disabled#if>>
    block






    delete



    td
    /td
    start td
    End td
    form content

    View Slide


  139. ${subscription.canceled?string('yes', 'no')}


    #list>





    <#if subscription.canceled>disabled#if>>
    block






    delete



    td
    /td
    start td
    End td
    formstart td
    /form td
    button

    View Slide


  140. ${subscription.canceled?string('yes', 'no')}


    #list>





    <#if subscription.canceled>disabled#if>>
    block






    delete



    td
    /td
    start td
    End td
    formstart td
    /form td
    inputs

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  144. Ryan

    View Slide

  145. Ryan

    View Slide

  146. Ktor
    • Web

    • Github

    • Slack

    http://ktor.io

    ktorio / ktor

    kotlinlang #ktor

    View Slide

  147. Ktor
    • Web

    • Github

    • Slack

    • Me
    http://ktor.io

    ktorio / ktor

    kotlinlang #ktor

    @rharter

    View Slide

  148. Servers ❤ Kotlin
    Ryan Harter
    @rharter

    View Slide