Slide 1

Slide 1 text

Servers ❤ Kotlin Ryan Harter @rharter

Slide 2

Slide 2 text

Ktor

Slide 3

Slide 3 text

Ktor Easy to use, fun and asynchronous.

Slide 4

Slide 4 text

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

Slide 5

Slide 5 text

Ktor Application

Slide 6

Slide 6 text

Ktor Application Jetty Netty Tomcat Servlet

Slide 7

Slide 7 text

Ktor Application Feature Feature Feature Feature Servlet

Slide 8

Slide 8 text

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

Slide 9

Slide 9 text

Ktor

Slide 10

Slide 10 text

Ktor Verify

Slide 11

Slide 11 text

Ktor Verify Admin

Slide 12

Slide 12 text

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

Slide 13

Slide 13 text

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

Slide 14

Slide 14 text

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

Slide 15

Slide 15 text

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

Slide 16

Slide 16 text

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

Slide 17

Slide 17 text

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

Slide 18

Slide 18 text

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

Slide 19

Slide 19 text

Typed Responses

Slide 20

Slide 20 text

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

Slide 21

Slide 21 text

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

Slide 22

Slide 22 text

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

Slide 23

Slide 23 text

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

Slide 24

Slide 24 text

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

Slide 25

Slide 25 text

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

Slide 26

Slide 26 text

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

Slide 27

Slide 27 text

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

Slide 28

Slide 28 text

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

Slide 29

Slide 29 text

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

Slide 30

Slide 30 text

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

Slide 31

Slide 31 text

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

Slide 32

Slide 32 text

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

Slide 33

Slide 33 text

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

Slide 34

Slide 34 text

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

Slide 35

Slide 35 text

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

Slide 36

Slide 36 text

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)

Slide 37

Slide 37 text

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)

Slide 38

Slide 38 text

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

Slide 39

Slide 39 text

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

Slide 40

Slide 40 text

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)

Slide 41

Slide 41 text

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

Slide 42

Slide 42 text

Typed Requests

Slide 43

Slide 43 text

No content

Slide 44

Slide 44 text

No content

Slide 45

Slide 45 text

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

Slide 46

Slide 46 text

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

Slide 47

Slide 47 text

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

Slide 48

Slide 48 text

@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

Slide 49

Slide 49 text

@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

Slide 50

Slide 50 text

@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

Slide 51

Slide 51 text

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

Slide 52

Slide 52 text

Slide 53

Slide 53 text

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

Slide 54

Slide 54 text

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

Slide 55

Slide 55 text

Slide 56

Slide 56 text

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

Slide 57

Slide 57 text

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

Slide 58

Slide 58 text

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

Slide 59

Slide 59 text

@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) ???

Slide 60

Slide 60 text

External Components

Slide 61

Slide 61 text

@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) ???

Slide 62

Slide 62 text

@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 ≈ ≈

Slide 63

Slide 63 text

@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 ≈ ≈

Slide 64

Slide 64 text

// 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 ≈ ≈

Slide 65

Slide 65 text

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

Slide 66

Slide 66 text

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

Slide 67

Slide 67 text

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

Slide 68

Slide 68 text

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

Slide 69

Slide 69 text

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 }

Slide 70

Slide 70 text

// 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

Slide 71

Slide 71 text

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

Slide 72

Slide 72 text

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

Slide 73

Slide 73 text

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

Slide 74

Slide 74 text

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

Slide 75

Slide 75 text

?: 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)

Slide 76

Slide 76 text

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

Slide 77

Slide 77 text

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

Slide 78

Slide 78 text

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

Slide 79

Slide 79 text

→ 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

Slide 80

Slide 80 text

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

Slide 81

Slide 81 text

Templates

Slide 82

Slide 82 text

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

Slide 83

Slide 83 text

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

Slide 84

Slide 84 text

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

Slide 85

Slide 85 text

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

Slide 86

Slide 86 text

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…

Slide 87

Slide 87 text

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

Slide 88

Slide 88 text

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

Slide 89

Slide 89 text

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

Slide 90

Slide 90 text

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

Slide 91

Slide 91 text

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

Slide 92

Slide 92 text

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

Slide 93

Slide 93 text

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

Slide 94

Slide 94 text

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

Slide 95

Slide 95 text

Home
search
Enter your query...
more_vert
  • About
  • Contact
  • Legal information
<#list subscriptions as subscription> Owner ID Start Date Expiry Date Cancelled ${subscription.ownerId} ${subscription.startDate?date} ${subscription.expiryDate?date} ${subscription.canceled?string('yes', 'no')}

Slide 96

Slide 96 text

Start Date Expiry Date Cancelled <#list subscriptions as subscription> ${subscription.ownerId} ${subscription.startDate?date} ${subscription.expiryDate?date} ${subscription.canceled?string('yes', 'no')} /table> >

Slide 97

Slide 97 text

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

Slide 98

Slide 98 text

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

Slide 99

Slide 99 text

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

Slide 100

Slide 100 text

Authentication

Slide 101

Slide 101 text

}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

Slide 102

Slide 102 text

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

Slide 103

Slide 103 text

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

Slide 104

Slide 104 text

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

Slide 105

Slide 105 text

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

Slide 106

Slide 106 text

}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

Slide 107

Slide 107 text

}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

Slide 108

Slide 108 text

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

Slide 109

Slide 109 text

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

Slide 110

Slide 110 text

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

Slide 111

Slide 111 text

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

Slide 112

Slide 112 text

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

Slide 113

Slide 113 text

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

Slide 114

Slide 114 text

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

Slide 115

Slide 115 text

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

Slide 116

Slide 116 text

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

Slide 117

Slide 117 text

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

Slide 118

Slide 118 text

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

Slide 119

Slide 119 text

Forms

Slide 120

Slide 120 text

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

Slide 121

Slide 121 text

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

Slide 122

Slide 122 text

Ryan

Slide 123

Slide 123 text

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

Slide 124

Slide 124 text

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

Slide 125

Slide 125 text

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

Slide 126

Slide 126 text

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

Slide 127

Slide 127 text

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

Slide 128

Slide 128 text

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

Slide 129

Slide 129 text

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

Slide 130

Slide 130 text

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

Slide 131

Slide 131 text

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

Slide 132

Slide 132 text

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

Slide 133

Slide 133 text

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

Slide 134

Slide 134 text

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

Slide 135

Slide 135 text

Expiry Date Cancelled <#list subscriptions as subscription> ${subscription.ownerId} ${subscription.startDate?date} ${subscription.expiryDate?date} ${subscription.canceled?string('yes', 'no')} /table> >

Slide 136

Slide 136 text

${subscription.canceled?string('yes', 'no')} disabled> block delete td /td

Slide 137

Slide 137 text

${subscription.canceled?string('yes', 'no')} disabled> block delete td /td start td End td

Slide 138

Slide 138 text

${subscription.canceled?string('yes', 'no')} disabled> block delete td /td start td End td form content

Slide 139

Slide 139 text

${subscription.canceled?string('yes', 'no')} disabled> block delete td /td start td End td formstart td /form td button

Slide 140

Slide 140 text

${subscription.canceled?string('yes', 'no')} disabled> block delete td /td start td End td formstart td /form td inputs

Slide 141

Slide 141 text

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

Slide 142

Slide 142 text

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

Slide 143

Slide 143 text

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

Slide 144

Slide 144 text

Ryan

Slide 145

Slide 145 text

Ryan

Slide 146

Slide 146 text

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

Slide 147

Slide 147 text

Ktor • Web • Github • Slack • Me http://ktor.io ktorio / ktor kotlinlang #ktor @rharter

Slide 148

Slide 148 text

Servers ❤ Kotlin Ryan Harter @rharter