diff --git a/src/main/kotlin/Minimalnotes.kt b/src/main/kotlin/Minimalnotes.kt index 83685a6..61a4e6f 100644 --- a/src/main/kotlin/Minimalnotes.kt +++ b/src/main/kotlin/Minimalnotes.kt @@ -1,76 +1,13 @@ package be.simplenotes import be.simplenotes.extensions.addShutdownHook -import be.simplenotes.extensions.hikariDataSource -import be.simplenotes.extensions.registerSerializers -import be.simplenotes.repositories.UserRepository -import be.simplenotes.routes.RouteSupplier -import be.simplenotes.routes.UserRoutes -import be.simplenotes.routes.toRouter -import be.simplenotes.security.Argon2PasswordHash -import be.simplenotes.security.PasswordHash -import be.simplenotes.security.SimpleJwt -import be.simplenotes.serializers.UserIdSerializer -import be.simplenotes.serializers.UserSerializer -import com.fasterxml.jackson.databind.ObjectMapper -import com.fasterxml.jackson.databind.ser.std.StdSerializer -import org.flywaydb.core.Flyway -import org.http4k.core.then -import org.http4k.filter.ServerFilters -import org.http4k.format.Jackson -import org.http4k.routing.RoutingHttpHandler -import org.http4k.server.ApacheServer -import org.http4k.server.asServer -import org.jooq.SQLDialect -import org.jooq.impl.DSL +import be.simplenotes.modules.httpModule +import be.simplenotes.modules.jacksonModule +import be.simplenotes.modules.persistanceModule +import be.simplenotes.modules.securityModule import org.koin.core.context.startKoin -import org.koin.dsl.bind -import org.koin.dsl.module -import org.koin.dsl.onClose -import java.util.concurrent.TimeUnit -import javax.sql.DataSource -fun main() { - val jacksonModule = module { - single { UserIdSerializer() } bind StdSerializer::class - single { UserSerializer() } bind StdSerializer::class - single { Jackson.mapper } - single(createdAtStart = true) { - get().registerSerializers(getAll()) - } - } +fun main() = startKoin { + modules(httpModule, securityModule, jacksonModule, persistanceModule) +}.addShutdownHook() - val persistanceModule = module { - single { UserRepository(get(), get()) } - - single { - hikariDataSource { - jdbcUrl = "jdbc:h2:./notes.db;MODE=MySQL;" - driverClassName = "org.h2.Driver" - maximumPoolSize = 2 - } - } bind DataSource::class onClose { it?.close() } - - single { Flyway.configure().dataSource(get()).load() } - single { get().migrate(); DSL.using(get(), SQLDialect.H2) } - } - - val securityModule = module { - single { SimpleJwt("super secret", 1, TimeUnit.HOURS) } - single { Argon2PasswordHash() } - } - - val httpModule = module { - single { UserRoutes(get(), get()) } bind RouteSupplier::class - single { getAll().toRouter() } - single(createdAtStart = true) { - ServerFilters.CatchLensFailure() - .then(get()) - .asServer(ApacheServer(7000)).start() - } onClose { it?.close() } - } - - startKoin { - modules(httpModule, securityModule, jacksonModule, persistanceModule) - }.koin.addShutdownHook() -} diff --git a/src/main/kotlin/extensions/KoinExtensions.kt b/src/main/kotlin/extensions/KoinExtensions.kt index deada47..c412f8c 100644 --- a/src/main/kotlin/extensions/KoinExtensions.kt +++ b/src/main/kotlin/extensions/KoinExtensions.kt @@ -1,9 +1,9 @@ package be.simplenotes.extensions -import org.koin.core.Koin +import org.koin.core.KoinApplication import kotlin.concurrent.thread -fun Koin.addShutdownHook() { +fun KoinApplication.addShutdownHook() { Runtime.getRuntime().addShutdownHook( thread(start = false) { close() diff --git a/src/main/kotlin/filters/JwtFilter.kt b/src/main/kotlin/filters/JwtFilter.kt new file mode 100644 index 0000000..4bd9c07 --- /dev/null +++ b/src/main/kotlin/filters/JwtFilter.kt @@ -0,0 +1,29 @@ +package be.simplenotes.filters + +import be.simplenotes.security.SimpleJwt +import org.http4k.core.Filter +import org.http4k.core.RequestContexts +import org.http4k.core.Response +import org.http4k.core.Status +import org.http4k.core.cookie.cookie + +class JwtFilter( + private val ctx: RequestContexts, + private val simpleJwt: SimpleJwt, +) { + private val authKey = "auth" + + operator fun invoke() = Filter { next -> + { req -> + val extractedToken = req.cookie("Bearer") + ?.value + ?.trim() + ?.let { simpleJwt.extract(it) } + + if (extractedToken != null) { + ctx[req][authKey] = extractedToken + next(req) + } else Response(Status.UNAUTHORIZED) + } + } +} diff --git a/src/main/kotlin/modules/HttpModule.kt b/src/main/kotlin/modules/HttpModule.kt new file mode 100644 index 0000000..5375ee8 --- /dev/null +++ b/src/main/kotlin/modules/HttpModule.kt @@ -0,0 +1,28 @@ +package be.simplenotes.modules + +import be.simplenotes.filters.JwtFilter +import be.simplenotes.routes.RouteSupplier +import be.simplenotes.routes.UserRoutes +import be.simplenotes.routes.toRouter +import org.http4k.core.RequestContexts +import org.http4k.core.then +import org.http4k.filter.ServerFilters +import org.http4k.routing.RoutingHttpHandler +import org.http4k.server.ApacheServer +import org.http4k.server.asServer +import org.koin.dsl.bind +import org.koin.dsl.module +import org.koin.dsl.onClose + +val httpModule = module { + single { UserRoutes(get(), get(), get(), get()) } bind RouteSupplier::class + single { getAll().toRouter() } + single { RequestContexts() } + single { JwtFilter(get(), get())() } + single(createdAtStart = true) { + ServerFilters.CatchLensFailure() + .then(ServerFilters.InitialiseRequestContext(get())) + .then(get()) + .asServer(ApacheServer(7000)).start() + } onClose { it?.close() } +} diff --git a/src/main/kotlin/modules/JacksonModule.kt b/src/main/kotlin/modules/JacksonModule.kt new file mode 100644 index 0000000..d15c5ed --- /dev/null +++ b/src/main/kotlin/modules/JacksonModule.kt @@ -0,0 +1,17 @@ +package be.simplenotes.modules + +import be.simplenotes.extensions.registerSerializers +import be.simplenotes.serializers.UserIdSerializer +import be.simplenotes.serializers.UserSerializer +import com.fasterxml.jackson.databind.ObjectMapper +import com.fasterxml.jackson.databind.ser.std.StdSerializer +import org.http4k.format.Jackson +import org.koin.dsl.bind +import org.koin.dsl.module + +val jacksonModule = module { + single { UserIdSerializer() } bind StdSerializer::class + single { UserSerializer() } bind StdSerializer::class + single { Jackson.mapper } + single(createdAtStart = true) { get().registerSerializers(getAll()) } +} diff --git a/src/main/kotlin/modules/PersistanceModule.kt b/src/main/kotlin/modules/PersistanceModule.kt new file mode 100644 index 0000000..8a745ac --- /dev/null +++ b/src/main/kotlin/modules/PersistanceModule.kt @@ -0,0 +1,26 @@ +package be.simplenotes.modules + +import be.simplenotes.extensions.hikariDataSource +import be.simplenotes.repositories.UserRepository +import org.flywaydb.core.Flyway +import org.jooq.SQLDialect +import org.jooq.impl.DSL +import org.koin.dsl.bind +import org.koin.dsl.module +import org.koin.dsl.onClose +import javax.sql.DataSource + +val persistanceModule = module { + single { UserRepository(get(), get()) } + + single { + hikariDataSource { + jdbcUrl = "jdbc:h2:./notes.db;MODE=MySQL;" + driverClassName = "org.h2.Driver" + maximumPoolSize = 2 + } + } bind DataSource::class onClose { it?.close() } + + single { Flyway.configure().dataSource(get()).load() } + single { get().migrate(); DSL.using(get(), SQLDialect.H2) } +} diff --git a/src/main/kotlin/modules/SecurityModule.kt b/src/main/kotlin/modules/SecurityModule.kt new file mode 100644 index 0000000..4ada943 --- /dev/null +++ b/src/main/kotlin/modules/SecurityModule.kt @@ -0,0 +1,12 @@ +package be.simplenotes.modules + +import be.simplenotes.security.Argon2PasswordHash +import be.simplenotes.security.PasswordHash +import be.simplenotes.security.SimpleJwt +import org.koin.dsl.module +import java.util.concurrent.TimeUnit + +val securityModule = module { + single { SimpleJwt("super secret", 1, TimeUnit.HOURS) } + single { Argon2PasswordHash() } +} diff --git a/src/main/kotlin/routes/UserRoutes.kt b/src/main/kotlin/routes/UserRoutes.kt index 8b7029c..ac1fdd2 100644 --- a/src/main/kotlin/routes/UserRoutes.kt +++ b/src/main/kotlin/routes/UserRoutes.kt @@ -6,16 +6,14 @@ import be.simplenotes.extensions.notFound import be.simplenotes.extensions.ok import be.simplenotes.repositories.UserRepository import be.simplenotes.security.SimpleJwt -import org.http4k.core.Body +import org.http4k.core.* import org.http4k.core.Method.* -import org.http4k.core.Response import org.http4k.core.cookie.Cookie import org.http4k.core.cookie.cookie -import org.http4k.core.with import org.http4k.format.Jackson.auto import org.http4k.lens.ContentNegotiation -import org.http4k.lens.Cookies import org.http4k.lens.Path +import org.http4k.lens.RequestContextKey import org.http4k.lens.long import org.http4k.routing.bind import org.http4k.routing.routes @@ -23,12 +21,17 @@ import org.http4k.routing.routes data class Message(val msg: String) data class Login(val username: String, val password: String) -class UserRoutes(private val userRepository: UserRepository, private val simpleJwt: SimpleJwt) : RouteSupplier { +class UserRoutes( + private val userRepository: UserRepository, + private val simpleJwt: SimpleJwt, + private val jwtFilter: Filter, + private val ctx: RequestContexts, +) : RouteSupplier { private val userLens = Body.auto("user", ContentNegotiation.StrictNoDirective).toLens() private val msgLens = Body.auto().toLens() private val loginLens = Body.auto().toLens() private val idLens = Path.long().map { UserId(it) }.of("id") - private val jwtLens = Cookies.optional("Bearer") + private val userIdLens = RequestContextKey.required(ctx, "auth") private fun userNotFound() = Response.notFound().with(msgLens of Message("not found")) @@ -57,11 +60,9 @@ class UserRoutes(private val userRepository: UserRepository, private val simpleJ Response.ok().with(userLens of user.copy(id = null)).cookie(cookie) } ?: Response.notFound() }, - "/whoami" bind GET to { - jwtLens(it)?.value - ?.let { token -> simpleJwt.extract(token) } - ?.let { id -> Response.ok().with(userLens of userRepository.find(id)!!) } - ?: Response.notFound() + "/private" bind GET to jwtFilter.then { + val user = userRepository.find(userIdLens(it))!! + Response.ok().with(msgLens of Message("Welcome $user")) } ) }