Skip to content

Commit fb6fed3

Browse files
committed
Coding tidy up and correctness fixes
1 parent 9e4ef14 commit fb6fed3

11 files changed

Lines changed: 144 additions & 131 deletions

File tree

src/main/kotlin/no/java/cupcake/Application.kt

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
package no.java.cupcake
22

33
import io.ktor.server.application.Application
4+
import io.ktor.server.application.ApplicationCallPipeline
45
import io.ktor.server.application.ApplicationEnvironment
56
import io.ktor.server.cio.EngineMain
7+
import kotlinx.coroutines.launch
68
import no.java.cupcake.bring.BringService
79
import no.java.cupcake.clients.bringClient
810
import no.java.cupcake.clients.sleepingPillClient
@@ -34,10 +36,13 @@ fun Application.module() {
3436
configureHTTP()
3537
val userInfoEndpoint = configureAuth(oidcConfig = environment.oidcConfig())
3638
configureUserInfoRoute(userInfoEndpoint = userInfoEndpoint)
39+
val sleepingPillService = sleepingPillService(bringService())
3740
configureRouting(
38-
sleepingPillService = sleepingPillService(bringService()),
41+
sleepingPillService = sleepingPillService,
3942
securityOptional = !environment.bool("jwt.enabled"),
4043
)
44+
val ready = launch { sleepingPillService.warmUp() }
45+
intercept(ApplicationCallPipeline.Setup) { ready.join() }
4146
}
4247

4348
private fun Application.sleepingPillService(bringService: BringService): SleepingPillService {

src/main/kotlin/no/java/cupcake/api/ApiError.kt

Lines changed: 57 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -11,38 +11,44 @@ data class ErrorResponse(
1111
val fieldValue: String? = null,
1212
)
1313

14-
sealed class ApiError(
15-
protected open val errorResponse: ErrorResponse,
16-
) {
17-
open fun messageMap(): Map<String, ErrorResponse> = mapOf("error" to errorResponse)
18-
19-
fun status() = this.errorResponse.status
14+
sealed interface ApiError {
15+
val response: ErrorResponse
2016
}
2117

18+
fun ApiError.status() = response.status
19+
20+
fun ApiError.messageMap(): Map<String, ErrorResponse> =
21+
when (this) {
22+
is UpstreamError -> mapOf("upstream" to upstream, "error" to response)
23+
24+
is RequiredField,
25+
is CallPrincipalMissing,
26+
is TokenMissing,
27+
is TokenMissingUser,
28+
is RefreshTokenInvalid,
29+
-> mapOf("error" to response)
30+
}
31+
2232
abstract class UpstreamError(
2333
open val upstream: ErrorResponse,
2434
val systemName: String,
25-
) : ApiError(
35+
) : ApiError {
36+
override val response =
2637
ErrorResponse(
2738
status = HttpStatusCode.InternalServerError,
2839
message = "call to $systemName failed",
29-
),
30-
) {
31-
override fun messageMap() =
32-
mapOf(
33-
"upstream" to upstream,
34-
"error" to errorResponse,
3540
)
3641
}
3742

3843
abstract class RequiredField(
3944
val fieldName: String,
40-
) : ApiError(
45+
) : ApiError {
46+
override val response =
4147
ErrorResponse(
4248
status = HttpStatusCode.BadRequest,
4349
message = "$fieldName required",
44-
),
45-
)
50+
)
51+
}
4652

4753
data object ConferenceIdRequired : RequiredField(fieldName = "id")
4854

@@ -53,30 +59,41 @@ data class SleepingPillCallFailed(
5359
systemName = "SleepingPill",
5460
)
5561

56-
data object CallPrincipalMissing : ApiError(
57-
ErrorResponse(
58-
status = HttpStatusCode.Unauthorized,
59-
message = "Principal missing",
60-
),
61-
)
62+
data class CognitoCallFailed(
63+
override val upstream: ErrorResponse,
64+
) : UpstreamError(
65+
upstream = upstream,
66+
systemName = "Cognito",
67+
)
6268

63-
data object TokenMissing : ApiError(
64-
ErrorResponse(
65-
status = HttpStatusCode.Unauthorized,
66-
message = "Principal missing token",
67-
),
68-
)
69+
data object CallPrincipalMissing : ApiError {
70+
override val response =
71+
ErrorResponse(
72+
status = HttpStatusCode.Unauthorized,
73+
message = "Principal missing",
74+
)
75+
}
6976

70-
data object TokenMissingUser : ApiError(
71-
ErrorResponse(
72-
status = HttpStatusCode.Unauthorized,
73-
message = "User missing in token",
74-
),
75-
)
77+
data object TokenMissing : ApiError {
78+
override val response =
79+
ErrorResponse(
80+
status = HttpStatusCode.Unauthorized,
81+
message = "Principal missing token",
82+
)
83+
}
7684

77-
data object RefreshTokenInvalid : ApiError(
78-
ErrorResponse(
79-
status = HttpStatusCode.Unauthorized,
80-
message = "Refresh token is invalid or has expired",
81-
),
82-
)
85+
data object TokenMissingUser : ApiError {
86+
override val response =
87+
ErrorResponse(
88+
status = HttpStatusCode.Unauthorized,
89+
message = "User missing in token",
90+
)
91+
}
92+
93+
data object RefreshTokenInvalid : ApiError {
94+
override val response =
95+
ErrorResponse(
96+
status = HttpStatusCode.Unauthorized,
97+
message = "Refresh token is invalid or has expired",
98+
)
99+
}

src/main/kotlin/no/java/cupcake/api/Respond.kt

Lines changed: 7 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -7,27 +7,15 @@ import io.ktor.server.response.respondRedirect
77
import io.ktor.server.routing.RoutingContext
88

99
context(context: RoutingContext)
10-
suspend inline fun <reified A : Any> Either<ApiError, A>.performResponse(
11-
status: HttpStatusCode = HttpStatusCode.OK,
12-
redirect: Boolean = false,
13-
) = when (this) {
14-
is Either.Left -> {
15-
context.respond(value)
16-
}
17-
18-
is Either.Right -> {
19-
when (redirect) {
20-
false -> context.call.respond(status, value)
21-
true -> context.call.respondRedirect(value.toString())
22-
}
23-
}
10+
suspend inline fun <reified A : Any> Either<ApiError, A>.respond(status: HttpStatusCode = HttpStatusCode.OK) {
11+
onLeft { context.respond(it) }
12+
onRight { context.call.respond(status, it) }
2413
}
2514

2615
context(context: RoutingContext)
27-
suspend inline fun <reified A : Any> Either<ApiError, A>.respond(status: HttpStatusCode = HttpStatusCode.OK) =
28-
performResponse(status, redirect = false)
29-
30-
context(context: RoutingContext)
31-
suspend inline fun <reified A : Any> Either<ApiError, A>.redirect() = performResponse(redirect = true)
16+
suspend inline fun <reified A : Any> Either<ApiError, A>.redirect() {
17+
onLeft { context.respond(it) }
18+
onRight { context.call.respondRedirect(it.toString()) }
19+
}
3220

3321
suspend fun RoutingContext.respond(error: ApiError) = call.respond(error.status(), error.messageMap())

src/main/kotlin/no/java/cupcake/bring/BringService.kt

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -44,11 +44,7 @@ class BringService(
4444
}
4545
}
4646

47-
fun getPostalCode(id: String?): PostalCode? {
48-
if (id == null) return null
49-
50-
return cache.get(id)
51-
}
47+
fun getPostalCode(id: String?) = id?.let(cache::get)
5248

5349
private fun scheduleRefresh() {
5450
logger.info { "Scheduling postal code cache refresh" }

src/main/kotlin/no/java/cupcake/plugins/HTTP.kt

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@ fun Application.configureHTTP() {
1414
allowMethod(HttpMethod.Delete)
1515
allowMethod(HttpMethod.Patch)
1616
allowHeader(HttpHeaders.Authorization)
17-
allowHeader("MyCustomHeader")
1817
anyHost()
1918
}
2019
install(Compression)

src/main/kotlin/no/java/cupcake/plugins/Routing.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ fun Application.configureRouting(
4242
get("/sessions") {
4343
either {
4444
sleepingPillService.sessions(
45-
id = ConferenceId(call.parameters["id"]).bind(),
45+
id = ConferenceId(call.parameters["id"]),
4646
)
4747
}.respond()
4848
}

src/main/kotlin/no/java/cupcake/plugins/Security.kt

Lines changed: 45 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
package no.java.cupcake.plugins
22

3+
import arrow.core.Either
4+
import arrow.core.raise.context.bind
5+
import arrow.core.raise.context.either
6+
import arrow.core.raise.context.ensureNotNull
37
import com.auth0.jwk.JwkProviderBuilder
48
import io.ktor.client.HttpClient
59
import io.ktor.client.call.body
@@ -17,14 +21,19 @@ import io.ktor.server.auth.authenticate
1721
import io.ktor.server.auth.jwt.JWTPrincipal
1822
import io.ktor.server.auth.jwt.jwt
1923
import io.ktor.server.auth.principal
20-
import io.ktor.server.response.respond
2124
import io.ktor.server.response.respondText
2225
import io.ktor.server.routing.get
2326
import io.ktor.server.routing.routing
2427
import kotlinx.coroutines.runBlocking
2528
import kotlinx.serialization.SerialName
2629
import kotlinx.serialization.Serializable
2730
import kotlinx.serialization.json.Json
31+
import no.java.cupcake.api.CallPrincipalMissing
32+
import no.java.cupcake.api.CognitoCallFailed
33+
import no.java.cupcake.api.ErrorResponse
34+
import no.java.cupcake.api.TokenMissing
35+
import no.java.cupcake.api.TokenMissingUser
36+
import no.java.cupcake.api.respond
2837
import no.java.cupcake.config.OidcConfig
2938
import java.net.URI
3039
import java.util.concurrent.TimeUnit
@@ -114,30 +123,44 @@ fun Application.configureUserInfoRoute(userInfoEndpoint: String) {
114123
routing {
115124
authenticate("javaBin") {
116125
get("/api/me") {
117-
val p = call.principal<JWTPrincipal>()!!
118-
val token =
119-
call.request.headers[HttpHeaders.Authorization]
120-
?.removePrefix("Bearer ") ?: ""
126+
either {
127+
val principal = ensureNotNull(call.principal<JWTPrincipal>()) { CallPrincipalMissing }
128+
val token =
129+
ensureNotNull(
130+
call.request.headers[HttpHeaders.Authorization]?.removePrefix("Bearer "),
131+
) { TokenMissing }
132+
133+
val groups =
134+
principal.payload
135+
.getClaim("cognito:groups")
136+
?.asList(String::class.java)
137+
.orEmpty()
138+
139+
val userInfo =
140+
Either
141+
.catch {
142+
http
143+
.get(userInfoEndpoint) {
144+
header(HttpHeaders.Authorization, "Bearer $token")
145+
}.body<UserInfoResponse>()
146+
}.mapLeft {
147+
CognitoCallFailed(
148+
ErrorResponse(
149+
HttpStatusCode.BadGateway,
150+
it.message ?: "userinfo call failed",
151+
),
152+
)
153+
}.bind()
154+
155+
val email = ensureNotNull(userInfo.email) { TokenMissingUser }
121156

122-
val groups =
123-
p.payload
124-
.getClaim("cognito:groups")
125-
?.asList(String::class.java) ?: emptyList()
126-
127-
val userInfo =
128-
http
129-
.get(userInfoEndpoint) {
130-
header(HttpHeaders.Authorization, "Bearer $token")
131-
}.body<UserInfoResponse>()
132-
133-
call.respond(
134157
UserInfo(
135-
sub = p.payload.subject,
136-
preferredUsername = userInfo.email ?: p.payload.subject,
137-
email = userInfo.email ?: "",
158+
sub = principal.payload.subject,
159+
preferredUsername = email,
160+
email = email,
138161
groups = groups,
139-
),
140-
)
162+
)
163+
}.respond()
141164
}
142165
}
143166
}

src/main/kotlin/no/java/cupcake/sleepingpill/Conference.kt

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
package no.java.cupcake.sleepingpill
22

3-
import arrow.core.raise.either
4-
import arrow.core.raise.ensure
3+
import arrow.core.raise.Raise
4+
import arrow.core.raise.context.ensureNotNull
55
import kotlinx.serialization.Serializable
6+
import no.java.cupcake.api.ApiError
67
import no.java.cupcake.api.ConferenceIdRequired
78
import java.time.Year
89

@@ -29,14 +30,13 @@ data class SleepingPillConferences(
2930
)
3031

3132
@Serializable
32-
data class ConferenceId private constructor(
33+
@JvmInline
34+
value class ConferenceId private constructor(
3335
val id: String,
3436
) {
3537
companion object {
38+
context(_: Raise<ApiError>)
3639
operator fun invoke(id: String?) =
37-
either {
38-
ensure(!id.isNullOrBlank()) { ConferenceIdRequired }
39-
ConferenceId(id)
40-
}
40+
ConferenceId(ensureNotNull(id?.takeIf { it.isNotBlank() }) { ConferenceIdRequired })
4141
}
4242
}

0 commit comments

Comments
 (0)