Switch from koin to micronaut-inject

This commit is contained in:
Hubert Van De Walle 2020-10-30 02:27:22 +01:00
parent 78b84dc62a
commit 6a43acfd46
70 changed files with 728 additions and 439 deletions

View File

@ -16,7 +16,6 @@ object Libs {
const val jbcrypt = "org.mindrot:jbcrypt:0.4" const val jbcrypt = "org.mindrot:jbcrypt:0.4"
const val jettyServer = "org.eclipse.jetty:jetty-server:9.4.32.v20200930" const val jettyServer = "org.eclipse.jetty:jetty-server:9.4.32.v20200930"
const val jettyServlet = "org.eclipse.jetty:jetty-servlet:9.4.32.v20200930" const val jettyServlet = "org.eclipse.jetty:jetty-servlet:9.4.32.v20200930"
const val koinCore = "org.koin:koin-core:2.1.6"
const val konform = "io.konform:konform-jvm:0.2.0" const val konform = "io.konform:konform-jvm:0.2.0"
const val kotlinxHtml = "org.jetbrains.kotlinx:kotlinx-html-jvm:0.7.1" const val kotlinxHtml = "org.jetbrains.kotlinx:kotlinx-html-jvm:0.7.1"
const val kotlinxSerializationJson = "org.jetbrains.kotlinx:kotlinx-serialization-json-jvm:1.0.0" const val kotlinxSerializationJson = "org.jetbrains.kotlinx:kotlinx-serialization-json-jvm:1.0.0"
@ -28,6 +27,8 @@ object Libs {
const val luceneQueryParser = "org.apache.lucene:lucene-queryparser:8.6.1" const val luceneQueryParser = "org.apache.lucene:lucene-queryparser:8.6.1"
const val mapstruct = "org.mapstruct:mapstruct:1.4.1.Final" const val mapstruct = "org.mapstruct:mapstruct:1.4.1.Final"
const val mapstructProcessor = "org.mapstruct:mapstruct-processor:1.4.1.Final" const val mapstructProcessor = "org.mapstruct:mapstruct-processor:1.4.1.Final"
const val micronaut = "io.micronaut:micronaut-inject:2.1.2"
const val micronautProcessor = "io.micronaut:micronaut-inject-java:2.1.2"
const val mariadbClient = "org.mariadb.jdbc:mariadb-java-client:2.6.2" const val mariadbClient = "org.mariadb.jdbc:mariadb-java-client:2.6.2"
const val owaspHtmlSanitizer = "com.googlecode.owasp-java-html-sanitizer:owasp-java-html-sanitizer:20200713.1" const val owaspHtmlSanitizer = "com.googlecode.owasp-java-html-sanitizer:owasp-java-html-sanitizer:20200713.1"
const val prettytime ="org.ocpsoft.prettytime:prettytime:4.0.5.Final" const val prettytime ="org.ocpsoft.prettytime:prettytime:4.0.5.Final"
@ -39,4 +40,6 @@ object Libs {
const val http4kTestingHamkrest = "org.http4k:http4k-testing-hamkrest:3.268.0" const val http4kTestingHamkrest = "org.http4k:http4k-testing-hamkrest:3.268.0"
const val junit = "org.junit.jupiter:junit-jupiter:5.6.2" const val junit = "org.junit.jupiter:junit-jupiter:5.6.2"
const val mockk = "io.mockk:mockk:1.10.0" const val mockk = "io.mockk:mockk:1.10.0"
const val faker = "com.github.javafaker:javafaker:1.0.2"
const val mariaTestContainer = "org.testcontainers:mariadb:1.15.0-rc2"
} }

View File

@ -6,6 +6,7 @@ plugins {
id("be.simplenotes.app-shadow") id("be.simplenotes.app-shadow")
id("be.simplenotes.app-css") id("be.simplenotes.app-css")
id("be.simplenotes.app-docker") id("be.simplenotes.app-docker")
kotlin("kapt")
} }
dependencies { dependencies {
@ -16,7 +17,6 @@ dependencies {
implementation(project(":simplenotes-config")) implementation(project(":simplenotes-config"))
implementation(project(":simplenotes-views")) implementation(project(":simplenotes-views"))
implementation(Libs.koinCore)
implementation(Libs.arrowCoreData) implementation(Libs.arrowCoreData)
implementation(Libs.konform) implementation(Libs.konform)
implementation(Libs.http4kCore) implementation(Libs.http4kCore)
@ -27,6 +27,12 @@ dependencies {
implementation(Libs.logbackClassic) implementation(Libs.logbackClassic)
implementation(Libs.ktormCore) implementation(Libs.ktormCore)
implementation(Libs.micronaut)
kapt(Libs.micronautProcessor)
testImplementation(Libs.micronaut)
kaptTest(Libs.micronautProcessor)
testImplementation(Libs.junit) testImplementation(Libs.junit)
testImplementation(Libs.assertJ) testImplementation(Libs.assertJ)
testImplementation(Libs.http4kTestingHamkrest) testImplementation(Libs.http4kTestingHamkrest)

View File

@ -1,21 +1,27 @@
package be.simplenotes.app package be.simplenotes.app
import io.micronaut.context.annotation.Context
import org.http4k.server.Http4kServer import org.http4k.server.Http4kServer
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import javax.annotation.PostConstruct
import javax.annotation.PreDestroy
import be.simplenotes.config.ServerConfig as SimpleNotesServerConfig import be.simplenotes.config.ServerConfig as SimpleNotesServerConfig
@Context
class Server( class Server(
private val config: SimpleNotesServerConfig, private val config: SimpleNotesServerConfig,
private val http4kServer: Http4kServer, private val http4kServer: Http4kServer,
) { ) {
private val logger = LoggerFactory.getLogger(javaClass) private val logger = LoggerFactory.getLogger(javaClass)
@PostConstruct
fun start(): Server { fun start(): Server {
http4kServer.start() http4kServer.start()
logger.info("Listening on http://${config.host}:${config.port}") logger.info("Listening on http://${config.host}:${config.port}")
return this return this
} }
@PreDestroy
fun stop() { fun stop() {
logger.info("Stopping server") logger.info("Stopping server")
http4kServer.close() http4kServer.close()

View File

@ -1,31 +1,12 @@
package be.simplenotes.app package be.simplenotes.app
import be.simplenotes.app.extensions.addShutdownHook import io.micronaut.context.ApplicationContext
import be.simplenotes.app.modules.*
import be.simplenotes.config.configModule
import be.simplenotes.domain.domainModule
import be.simplenotes.persistance.migrationModule
import be.simplenotes.persistance.persistanceModule
import be.simplenotes.search.searchModule
import be.simplenotes.views.viewModule
import org.koin.core.context.startKoin
import org.koin.core.context.unloadKoinModules
fun main() { fun main() {
startKoin { val ctx = ApplicationContext.run().start()
modules( Runtime.getRuntime().addShutdownHook(
serverModule, Thread {
persistanceModule, ctx.stop()
migrationModule, }
configModule, )
viewModule,
controllerModule,
domainModule,
searchModule,
apiModule,
jsonModule
)
}.addShutdownHook()
unloadKoinModules(listOf(migrationModule, configModule))
} }

View File

@ -17,8 +17,13 @@ import org.http4k.core.Status.Companion.OK
import org.http4k.lens.Path import org.http4k.lens.Path
import org.http4k.lens.uuid import org.http4k.lens.uuid
import java.util.* import java.util.*
import javax.inject.Singleton
class ApiNoteController(private val noteService: NoteService, private val json: Json) { @Singleton
class ApiNoteController(
json: Json,
private val noteService: NoteService,
) {
fun createNote(request: Request, loggedInUser: LoggedInUser): Response { fun createNote(request: Request, loggedInUser: LoggedInUser): Response {
val content = noteContentLens(request) val content = noteContentLens(request)

View File

@ -9,8 +9,13 @@ import org.http4k.core.Request
import org.http4k.core.Response import org.http4k.core.Response
import org.http4k.core.Status.Companion.BAD_REQUEST import org.http4k.core.Status.Companion.BAD_REQUEST
import org.http4k.core.Status.Companion.OK import org.http4k.core.Status.Companion.OK
import javax.inject.Singleton
class ApiUserController(private val userService: UserService, private val json: Json) { @Singleton
class ApiUserController(
json: Json,
private val userService: UserService,
) {
private val tokenLens = json.auto<Token>().toLens() private val tokenLens = json.auto<Token>().toLens()
private val loginFormLens = json.auto<LoginForm>().toLens() private val loginFormLens = json.auto<LoginForm>().toLens()

View File

@ -6,7 +6,9 @@ import be.simplenotes.views.BaseView
import org.http4k.core.Request import org.http4k.core.Request
import org.http4k.core.Response import org.http4k.core.Response
import org.http4k.core.Status.Companion.OK import org.http4k.core.Status.Companion.OK
import javax.inject.Singleton
@Singleton
class BaseController(private val view: BaseView) { class BaseController(private val view: BaseView) {
fun index(@Suppress("UNUSED_PARAMETER") request: Request, loggedInUser: LoggedInUser?) = fun index(@Suppress("UNUSED_PARAMETER") request: Request, loggedInUser: LoggedInUser?) =
Response(OK).html(view.renderHome(loggedInUser)) Response(OK).html(view.renderHome(loggedInUser))

View File

@ -5,7 +5,9 @@ import org.http4k.core.Request
import org.http4k.core.Response import org.http4k.core.Response
import org.http4k.core.Status.Companion.OK import org.http4k.core.Status.Companion.OK
import org.http4k.core.Status.Companion.SERVICE_UNAVAILABLE import org.http4k.core.Status.Companion.SERVICE_UNAVAILABLE
import javax.inject.Singleton
@Singleton
class HealthCheckController(private val dbHealthCheck: DbHealthCheck) { class HealthCheckController(private val dbHealthCheck: DbHealthCheck) {
fun healthCheck(@Suppress("UNUSED_PARAMETER") request: Request) = fun healthCheck(@Suppress("UNUSED_PARAMETER") request: Request) =
if (dbHealthCheck.isOk()) Response(OK) else Response(SERVICE_UNAVAILABLE) if (dbHealthCheck.isOk()) Response(OK) else Response(SERVICE_UNAVAILABLE)

View File

@ -18,8 +18,10 @@ import org.http4k.core.Status.Companion.OK
import org.http4k.core.body.form import org.http4k.core.body.form
import org.http4k.routing.path import org.http4k.routing.path
import java.util.* import java.util.*
import javax.inject.Singleton
import kotlin.math.abs import kotlin.math.abs
@Singleton
class NoteController( class NoteController(
private val view: NoteView, private val view: NoteView,
private val noteService: NoteService, private val noteService: NoteService,

View File

@ -10,7 +10,9 @@ import be.simplenotes.views.SettingView
import org.http4k.core.* import org.http4k.core.*
import org.http4k.core.body.form import org.http4k.core.body.form
import org.http4k.core.cookie.invalidateCookie import org.http4k.core.cookie.invalidateCookie
import javax.inject.Singleton
@Singleton
class SettingsController( class SettingsController(
private val userService: UserService, private val userService: UserService,
private val settingView: SettingView, private val settingView: SettingView,

View File

@ -21,7 +21,9 @@ import org.http4k.core.cookie.SameSite
import org.http4k.core.cookie.cookie import org.http4k.core.cookie.cookie
import org.http4k.core.cookie.invalidateCookie import org.http4k.core.cookie.invalidateCookie
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
import javax.inject.Singleton
@Singleton
class UserController( class UserController(
private val userService: UserService, private val userService: UserService,
private val userView: UserView, private val userView: UserView,

View File

@ -1,12 +0,0 @@
package be.simplenotes.app.extensions
import org.koin.core.KoinApplication
import kotlin.concurrent.thread
fun KoinApplication.addShutdownHook() {
Runtime.getRuntime().addShutdownHook(
thread(start = false) {
close()
}
)
}

View File

@ -1,58 +0,0 @@
package be.simplenotes.app.filters
import be.simplenotes.app.extensions.redirect
import be.simplenotes.domain.security.JwtPayloadExtractor
import be.simplenotes.types.LoggedInUser
import org.http4k.core.*
import org.http4k.core.Status.Companion.UNAUTHORIZED
import org.http4k.core.cookie.cookie
enum class AuthType {
Optional, Required
}
private const val authKey = "auth"
class AuthFilter(
private val extractor: JwtPayloadExtractor,
private val authType: AuthType,
private val ctx: RequestContexts,
private val source: JwtSource = JwtSource.Cookie,
private val redirect: Boolean = true,
) : Filter {
override fun invoke(next: HttpHandler): HttpHandler = {
val token = when (source) {
JwtSource.Header -> it.bearerTokenHeader()
JwtSource.Cookie -> it.bearerTokenCookie()
}
val jwtPayload = token?.let { extractor(token) }
when {
jwtPayload != null -> {
ctx[it][authKey] = jwtPayload
next(it)
}
authType == AuthType.Required -> {
if (redirect) Response.redirect("/login")
else Response(UNAUTHORIZED)
}
else -> next(it)
}
}
}
fun Request.jwtPayload(ctx: RequestContexts): LoggedInUser? = ctx[this][authKey]
enum class JwtSource {
Header, Cookie
}
private fun Request.bearerTokenCookie(): String? = cookie("Bearer")
?.value
?.trim()
private fun Request.bearerTokenHeader(): String? =
header("Authorization")
?.trim()
?.takeIf { it.startsWith("Bearer") }
?.substringAfter("Bearer")
?.trim()

View File

@ -10,7 +10,9 @@ import org.http4k.core.Status.Companion.NOT_IMPLEMENTED
import org.http4k.core.Status.Companion.SERVICE_UNAVAILABLE import org.http4k.core.Status.Companion.SERVICE_UNAVAILABLE
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import java.sql.SQLTransientException import java.sql.SQLTransientException
import javax.inject.Singleton
@Singleton
class ErrorFilter(private val errorView: ErrorView) : Filter { class ErrorFilter(private val errorView: ErrorView) : Filter {
private val logger = LoggerFactory.getLogger(javaClass) private val logger = LoggerFactory.getLogger(javaClass)

View File

@ -3,7 +3,9 @@ package be.simplenotes.app.filters
import me.liuwj.ktorm.database.Database import me.liuwj.ktorm.database.Database
import org.http4k.core.Filter import org.http4k.core.Filter
import org.http4k.core.HttpHandler import org.http4k.core.HttpHandler
import javax.inject.Singleton
@Singleton
class TransactionFilter(private val db: Database) : Filter { class TransactionFilter(private val db: Database) : Filter {
override fun invoke(next: HttpHandler): HttpHandler = { request -> override fun invoke(next: HttpHandler): HttpHandler = { request ->
db.useTransaction { db.useTransaction {

View File

@ -0,0 +1,24 @@
package be.simplenotes.app.filters.auth
import be.simplenotes.types.LoggedInUser
import org.http4k.core.Request
import org.http4k.core.cookie.cookie
import org.http4k.lens.BiDiLens
typealias OptionalAuthLens = BiDiLens<@JvmSuppressWildcards Request, @JvmSuppressWildcards LoggedInUser?>
typealias RequiredAuthLens = BiDiLens<@JvmSuppressWildcards Request, @JvmSuppressWildcards LoggedInUser>
enum class JwtSource {
Header, Cookie
}
fun Request.bearerTokenCookie(): String? = cookie("Bearer")
?.value
?.trim()
fun Request.bearerTokenHeader(): String? =
header("Authorization")
?.trim()
?.takeIf { it.startsWith("Bearer") }
?.substringAfter("Bearer")
?.trim()

View File

@ -0,0 +1,22 @@
package be.simplenotes.app.filters.auth
import be.simplenotes.app.filters.auth.JwtSource.Cookie
import be.simplenotes.domain.security.JwtPayloadExtractor
import org.http4k.core.Filter
import org.http4k.core.HttpHandler
import org.http4k.core.with
class OptionalAuthFilter(
private val extractor: JwtPayloadExtractor,
private val lens: OptionalAuthLens,
private val source: JwtSource = Cookie,
) : Filter {
override fun invoke(next: HttpHandler): HttpHandler = {
val token = when (source) {
JwtSource.Header -> it.bearerTokenHeader()
Cookie -> it.bearerTokenCookie()
}
next(it.with(lens of token?.let { extractor(it) }))
}
}

View File

@ -0,0 +1,30 @@
package be.simplenotes.app.filters.auth
import be.simplenotes.app.extensions.redirect
import be.simplenotes.domain.security.JwtPayloadExtractor
import org.http4k.core.Filter
import org.http4k.core.HttpHandler
import org.http4k.core.Response
import org.http4k.core.Status.Companion.UNAUTHORIZED
import org.http4k.core.with
class RequiredAuthFilter(
private val extractor: JwtPayloadExtractor,
private val lens: RequiredAuthLens,
private val source: JwtSource = JwtSource.Cookie,
private val redirect: Boolean = true,
) : Filter {
override fun invoke(next: HttpHandler): HttpHandler = {
val token = when (source) {
JwtSource.Header -> it.bearerTokenHeader()
JwtSource.Cookie -> it.bearerTokenCookie()
}
val jwtPayload = token?.let { extractor(token) }
if (jwtPayload != null) next(it.with(lens of jwtPayload))
else {
if (redirect) Response.redirect("/login")
else Response(UNAUTHORIZED)
}
}
}

View File

@ -1,24 +0,0 @@
package be.simplenotes.app.modules
import be.simplenotes.app.api.ApiNoteController
import be.simplenotes.app.api.ApiUserController
import be.simplenotes.app.filters.AuthFilter
import be.simplenotes.app.filters.AuthType
import be.simplenotes.app.filters.JwtSource
import org.http4k.core.Filter
import org.koin.core.qualifier.named
import org.koin.dsl.module
val apiModule = module {
single { ApiUserController(get(), get()) }
single { ApiNoteController(get(), get()) }
single<Filter>(named("apiAuthFilter")) {
AuthFilter(
extractor = get(),
authType = AuthType.Required,
ctx = get(),
source = JwtSource.Header,
redirect = false
)
}
}

View File

@ -0,0 +1,46 @@
package be.simplenotes.app.modules
import be.simplenotes.app.filters.auth.*
import be.simplenotes.domain.security.JwtPayloadExtractor
import io.micronaut.context.annotation.Factory
import io.micronaut.context.annotation.Primary
import org.http4k.core.RequestContexts
import org.http4k.lens.RequestContextKey
import javax.inject.Named
import javax.inject.Singleton
@Factory
class AuthModule {
@Singleton
@Named("optional")
fun optionalAuthLens(ctx: RequestContexts): OptionalAuthLens = RequestContextKey.optional(ctx)
@Singleton
@Named("required")
fun requiredAuthLens(ctx: RequestContexts): RequiredAuthLens = RequestContextKey.required(ctx)
@Singleton
fun optionalAuth(extractor: JwtPayloadExtractor, @Named("optional") lens: OptionalAuthLens) =
OptionalAuthFilter(extractor, lens)
@Primary
@Singleton
fun requiredAuth(extractor: JwtPayloadExtractor, @Named("required") lens: RequiredAuthLens) =
RequiredAuthFilter(extractor, lens)
@Singleton
@Named("api")
internal fun apiAuthFilter(
jwtPayloadExtractor: JwtPayloadExtractor,
@Named("required") lens: RequiredAuthLens,
) = RequiredAuthFilter(
extractor = jwtPayloadExtractor,
lens = lens,
source = JwtSource.Header,
redirect = false
)
@Singleton
fun requestContexts() = RequestContexts()
}

View File

@ -1,12 +0,0 @@
package be.simplenotes.app.modules
import be.simplenotes.app.controllers.*
import org.koin.dsl.module
val controllerModule = module {
single { UserController(get(), get(), get()) }
single { HealthCheckController(get()) }
single { BaseController(get()) }
single { NoteController(get(), get()) }
single { SettingsController(get(), get()) }
}

View File

@ -2,21 +2,20 @@ package be.simplenotes.app.modules
import be.simplenotes.app.serialization.LocalDateTimeSerializer import be.simplenotes.app.serialization.LocalDateTimeSerializer
import be.simplenotes.app.serialization.UuidSerializer import be.simplenotes.app.serialization.UuidSerializer
import io.micronaut.context.annotation.Factory
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import kotlinx.serialization.modules.SerializersModule import kotlinx.serialization.modules.SerializersModule
import org.koin.dsl.module
import java.time.LocalDateTime import java.time.LocalDateTime
import java.util.* import java.util.*
import javax.inject.Singleton
val jsonModule = module { @Factory
single { class JsonModule {
Json {
prettyPrint = true @Singleton
serializersModule = get() fun json() = Json {
} prettyPrint = true
} serializersModule = SerializersModule {
single {
SerializersModule {
contextual(LocalDateTime::class, LocalDateTimeSerializer()) contextual(LocalDateTime::class, LocalDateTimeSerializer())
contextual(UUID::class, UuidSerializer()) contextual(UUID::class, UuidSerializer())
} }

View File

@ -1,62 +1,38 @@
package be.simplenotes.app.modules package be.simplenotes.app.modules
import be.simplenotes.app.Server
import be.simplenotes.app.filters.AuthFilter
import be.simplenotes.app.filters.AuthType
import be.simplenotes.app.filters.ErrorFilter
import be.simplenotes.app.filters.TransactionFilter
import be.simplenotes.app.jetty.ConnectorBuilder import be.simplenotes.app.jetty.ConnectorBuilder
import be.simplenotes.app.jetty.Jetty import be.simplenotes.app.jetty.Jetty
import be.simplenotes.app.routes.Router import be.simplenotes.app.routes.Router
import be.simplenotes.app.utils.StaticFileResolver import be.simplenotes.app.utils.StaticFileResolver
import be.simplenotes.app.utils.StaticFileResolverImpl
import be.simplenotes.config.ServerConfig import be.simplenotes.config.ServerConfig
import io.micronaut.context.annotation.Factory
import org.eclipse.jetty.server.ServerConnector import org.eclipse.jetty.server.ServerConnector
import org.http4k.core.Filter import org.http4k.server.Http4kServer
import org.http4k.core.RequestContexts
import org.http4k.routing.RoutingHttpHandler
import org.http4k.server.asServer import org.http4k.server.asServer
import org.koin.core.qualifier.named import javax.inject.Named
import org.koin.core.qualifier.qualifier import javax.inject.Singleton
import org.koin.dsl.module import org.eclipse.jetty.server.Server as JettyServer
import org.koin.dsl.onClose
import org.http4k.server.ServerConfig as Http4kServerConfig import org.http4k.server.ServerConfig as Http4kServerConfig
val serverModule = module { @Factory
single(createdAtStart = true) { Server(get(), get()).start() } onClose { it?.stop() } class ServerModule {
single { get<RoutingHttpHandler>().asServer(get()) }
single<Http4kServerConfig> { @Singleton
val config = get<ServerConfig>() @Named("styles")
val builder: ConnectorBuilder = { server: org.eclipse.jetty.server.Server -> fun styles(resolver: StaticFileResolver) = resolver.resolve("styles.css")!!
@Singleton
fun http4kServer(router: Router, serverConfig: Http4kServerConfig): Http4kServer =
router().asServer(serverConfig)
@Singleton
fun http4kServerConfig(config: ServerConfig): Http4kServerConfig {
val builder: ConnectorBuilder = { server: JettyServer ->
ServerConnector(server).apply { ServerConnector(server).apply {
port = config.port port = config.port
host = config.host host = config.host
} }
} }
Jetty(config.port, builder) return Jetty(config.port, builder)
} }
single<StaticFileResolver> { StaticFileResolverImpl(get()) }
single {
Router(
get(),
get(),
get(),
get(),
get(),
get(),
get(),
requiredAuth = get(AuthType.Required.qualifier),
optionalAuth = get(AuthType.Optional.qualifier),
apiAuth = get(named("apiAuthFilter")),
get(),
get(),
get(),
)()
}
single { RequestContexts() }
single<Filter>(AuthType.Optional.qualifier) { AuthFilter(get(), AuthType.Optional, get()) }
single<Filter>(AuthType.Required.qualifier) { AuthFilter(get(), AuthType.Required, get()) }
single { ErrorFilter(get()) }
single { TransactionFilter(get()) }
single(named("styles")) { get<StaticFileResolver>().resolve("styles.css") }
} }

View File

@ -0,0 +1,54 @@
package be.simplenotes.app.routes
import be.simplenotes.app.api.ApiNoteController
import be.simplenotes.app.api.ApiUserController
import be.simplenotes.app.filters.TransactionFilter
import be.simplenotes.app.filters.auth.RequiredAuthFilter
import be.simplenotes.app.filters.auth.RequiredAuthLens
import org.http4k.core.Filter
import org.http4k.core.Method.*
import org.http4k.core.Request
import org.http4k.core.then
import org.http4k.routing.PathMethod
import org.http4k.routing.RoutingHttpHandler
import org.http4k.routing.bind
import org.http4k.routing.routes
import java.util.function.Supplier
import javax.inject.Named
import javax.inject.Singleton
@Singleton
class ApiRoutes(
private val apiUserController: ApiUserController,
private val apiNoteController: ApiNoteController,
private val transaction: TransactionFilter,
@Named("api") private val auth: RequiredAuthFilter,
@Named("required") private val authLens: RequiredAuthLens,
) : Supplier<RoutingHttpHandler> {
override fun get(): RoutingHttpHandler {
fun Filter.then(next: ProtectedHandler) = then { req: Request ->
next(req, authLens(req))
}
infix fun PathMethod.to(action: ProtectedHandler) =
this to { req: Request -> action(req, authLens(req)) }
return routes(
"/login" bind POST to apiUserController::login,
with(apiNoteController) {
auth.then(
routes(
"/" bind GET to ::notes,
"/" bind POST to transaction.then(::createNote),
"/search" bind POST to ::search,
"/{uuid}" bind GET to ::note,
"/{uuid}" bind PUT to transaction.then(::note),
)
).withBasePath("/notes")
}
).withBasePath("/api")
}
}

View File

@ -0,0 +1,67 @@
package be.simplenotes.app.routes
import be.simplenotes.app.controllers.BaseController
import be.simplenotes.app.controllers.HealthCheckController
import be.simplenotes.app.controllers.NoteController
import be.simplenotes.app.controllers.UserController
import be.simplenotes.app.filters.ImmutableFilter
import be.simplenotes.app.filters.TransactionFilter
import be.simplenotes.app.filters.auth.OptionalAuthFilter
import be.simplenotes.app.filters.auth.OptionalAuthLens
import org.http4k.core.ContentType
import org.http4k.core.Filter
import org.http4k.core.Method.GET
import org.http4k.core.Method.POST
import org.http4k.core.Request
import org.http4k.core.then
import org.http4k.routing.*
import java.util.function.Supplier
import javax.inject.Named
import javax.inject.Singleton
@Singleton
class BasicRoutes(
private val healthCheckController: HealthCheckController,
private val baseCtrl: BaseController,
private val userCtrl: UserController,
private val noteCtrl: NoteController,
@Named("optional") private val authLens: OptionalAuthLens,
private val auth: OptionalAuthFilter,
private val transactionFilter: TransactionFilter,
) : Supplier<RoutingHttpHandler> {
override fun get(): RoutingHttpHandler {
fun Filter.then(next: PublicHandler) = then { req: Request ->
next(req, authLens(req))
}
infix fun PathMethod.to(action: PublicHandler) =
this to { req: Request -> action(req, authLens(req)) }
val staticHandler = ImmutableFilter.then(
static(
ResourceLoader.Classpath("/static"),
"woff2" to ContentType("font/woff2"),
"webmanifest" to ContentType("application/manifest+json")
)
)
return routes(
auth.then(
routes(
"/" bind GET to baseCtrl::index,
"/register" bind GET to userCtrl::register,
"/register" bind POST to transactionFilter.then(userCtrl::register),
"/login" bind GET to userCtrl::login,
"/login" bind POST to userCtrl::login,
"/logout" bind POST to userCtrl::logout,
"/notes/public/{uuid}" bind GET to noteCtrl::public,
)
),
"/health" bind GET to healthCheckController::healthCheck,
staticHandler
)
}
}

View File

@ -0,0 +1,53 @@
package be.simplenotes.app.routes
import be.simplenotes.app.controllers.NoteController
import be.simplenotes.app.filters.TransactionFilter
import be.simplenotes.app.filters.auth.RequiredAuthFilter
import be.simplenotes.app.filters.auth.RequiredAuthLens
import org.http4k.core.Filter
import org.http4k.core.Method.GET
import org.http4k.core.Method.POST
import org.http4k.core.Request
import org.http4k.core.then
import org.http4k.routing.PathMethod
import org.http4k.routing.RoutingHttpHandler
import org.http4k.routing.bind
import org.http4k.routing.routes
import java.util.function.Supplier
import javax.inject.Named
import javax.inject.Singleton
@Singleton
class NoteRoutes(
private val noteCtrl: NoteController,
private val transaction: TransactionFilter,
private val auth: RequiredAuthFilter,
@Named("required") private val authLens: RequiredAuthLens,
) : Supplier<RoutingHttpHandler> {
override fun get(): RoutingHttpHandler {
fun Filter.then(next: ProtectedHandler) = then { req: Request ->
next(req, authLens(req))
}
infix fun PathMethod.to(action: ProtectedHandler) =
this to { req: Request -> action(req, authLens(req)) }
return auth.then(
with(noteCtrl) {
routes(
"/" bind GET to ::list,
"/" bind POST to ::search,
"/new" bind GET to ::new,
"/new" bind POST to transaction.then(::new),
"/trash" bind GET to ::trash,
"/{uuid}" bind GET to ::note,
"/{uuid}" bind POST to transaction.then(::note),
"/{uuid}/edit" bind GET to ::edit,
"/{uuid}/edit" bind POST to transaction.then(::edit),
"/deleted/{uuid}" bind POST to transaction.then(::deleted),
).withBasePath("/notes")
}
)
}
}

View File

@ -0,0 +1,8 @@
package be.simplenotes.app.routes
import be.simplenotes.types.LoggedInUser
import org.http4k.core.Request
import org.http4k.core.Response
internal typealias PublicHandler = (Request, LoggedInUser?) -> Response
internal typealias ProtectedHandler = (Request, LoggedInUser) -> Response

View File

@ -1,112 +1,32 @@
package be.simplenotes.app.routes package be.simplenotes.app.routes
import be.simplenotes.app.api.ApiNoteController import be.simplenotes.app.filters.ErrorFilter
import be.simplenotes.app.api.ApiUserController import be.simplenotes.app.filters.SecurityFilter
import be.simplenotes.app.controllers.* import org.http4k.core.RequestContexts
import be.simplenotes.app.filters.* import org.http4k.core.then
import be.simplenotes.types.LoggedInUser
import org.http4k.core.*
import org.http4k.core.Method.*
import org.http4k.filter.ResponseFilters.GZip import org.http4k.filter.ResponseFilters.GZip
import org.http4k.filter.ServerFilters.InitialiseRequestContext import org.http4k.filter.ServerFilters.InitialiseRequestContext
import org.http4k.routing.* import org.http4k.routing.RoutingHttpHandler
import org.http4k.routing.ResourceLoader.Companion.Classpath import org.http4k.routing.routes
import java.util.function.Supplier
import javax.inject.Singleton
@Singleton
class Router( class Router(
private val baseController: BaseController,
private val userController: UserController,
private val noteController: NoteController,
private val settingsController: SettingsController,
private val apiUserController: ApiUserController,
private val apiNoteController: ApiNoteController,
private val healthCheckController: HealthCheckController,
private val requiredAuth: Filter,
private val optionalAuth: Filter,
private val apiAuth: Filter,
private val errorFilter: ErrorFilter, private val errorFilter: ErrorFilter,
private val transactionFilter: TransactionFilter,
private val contexts: RequestContexts, private val contexts: RequestContexts,
private val subRouters: List<Supplier<RoutingHttpHandler>>,
) { ) {
operator fun invoke(): RoutingHttpHandler { operator fun invoke(): RoutingHttpHandler {
val basicRoutes =
routes(
"/health" bind GET to healthCheckController::healthCheck,
ImmutableFilter.then(
static(
Classpath("/static"),
"woff2" to ContentType("font/woff2"),
"webmanifest" to ContentType("application/manifest+json")
)
)
)
val publicRoutes = routes(
"/" bind GET public baseController::index,
"/register" bind GET public userController::register,
"/register" bind POST `public transactional` userController::register,
"/login" bind GET public userController::login,
"/login" bind POST public userController::login,
"/logout" bind POST to userController::logout,
"/notes/public/{uuid}" bind GET public noteController::public,
)
val protectedRoutes = routes(
"/settings" bind GET protected settingsController::settings,
"/settings" bind POST transactional settingsController::settings,
"/export" bind POST protected settingsController::export,
"/notes" bind GET protected noteController::list,
"/notes" bind POST protected noteController::search,
"/notes/new" bind GET protected noteController::new,
"/notes/new" bind POST transactional noteController::new,
"/notes/trash" bind GET protected noteController::trash,
"/notes/{uuid}" bind GET protected noteController::note,
"/notes/{uuid}" bind POST transactional noteController::note,
"/notes/{uuid}/edit" bind GET protected noteController::edit,
"/notes/{uuid}/edit" bind POST transactional noteController::edit,
"/notes/deleted/{uuid}" bind POST transactional noteController::deleted,
)
val apiRoutes = routes(
"/api/login" bind POST to apiUserController::login,
)
val protectedApiRoutes = routes(
"/api/notes" bind GET protected apiNoteController::notes,
"/api/notes" bind POST transactional apiNoteController::createNote,
"/api/notes/search" bind POST transactional apiNoteController::search,
"/api/notes/{uuid}" bind GET protected apiNoteController::note,
"/api/notes/{uuid}" bind PUT transactional apiNoteController::update,
)
val routes = routes( val routes = routes(
basicRoutes, *subRouters.map { it.get() }.toTypedArray()
optionalAuth.then(publicRoutes),
requiredAuth.then(protectedRoutes),
apiAuth.then(protectedApiRoutes),
apiRoutes,
) )
val globalFilters = errorFilter return errorFilter
.then(InitialiseRequestContext(contexts)) .then(InitialiseRequestContext(contexts))
.then(SecurityFilter) .then(SecurityFilter)
.then(GZip()) .then(GZip())
.then(routes)
return globalFilters.then(routes)
} }
private inline infix fun PathMethod.public(crossinline handler: PublicHandler) =
this to { handler(it, it.jwtPayload(contexts)) }
private inline infix fun PathMethod.protected(crossinline handler: ProtectedHandler) =
this to { handler(it, it.jwtPayload(contexts)!!) }
private inline infix fun PathMethod.transactional(crossinline handler: ProtectedHandler) =
this to transactionFilter.then { handler(it, it.jwtPayload(contexts)!!) }
private inline infix fun PathMethod.`public transactional`(crossinline handler: PublicHandler) =
this to transactionFilter.then { handler(it, it.jwtPayload(contexts)) }
} }
private typealias PublicHandler = (Request, LoggedInUser?) -> Response
private typealias ProtectedHandler = (Request, LoggedInUser) -> Response

View File

@ -0,0 +1,44 @@
package be.simplenotes.app.routes
import be.simplenotes.app.controllers.SettingsController
import be.simplenotes.app.filters.TransactionFilter
import be.simplenotes.app.filters.auth.RequiredAuthFilter
import be.simplenotes.app.filters.auth.RequiredAuthLens
import org.http4k.core.Filter
import org.http4k.core.Method.GET
import org.http4k.core.Method.POST
import org.http4k.core.Request
import org.http4k.core.then
import org.http4k.routing.PathMethod
import org.http4k.routing.RoutingHttpHandler
import org.http4k.routing.bind
import org.http4k.routing.routes
import java.util.function.Supplier
import javax.inject.Named
import javax.inject.Singleton
@Singleton
class SettingsRoutes(
private val settingsController: SettingsController,
private val transaction: TransactionFilter,
private val auth: RequiredAuthFilter,
@Named("required") private val authLens: RequiredAuthLens,
) : Supplier<RoutingHttpHandler> {
override fun get(): RoutingHttpHandler {
fun Filter.then(next: ProtectedHandler) = then { req: Request ->
next(req, authLens(req))
}
infix fun PathMethod.to(action: ProtectedHandler) =
this to { req: Request -> action(req, authLens(req)) }
return auth.then(
routes(
"/settings" bind GET to settingsController::settings,
"/settings" bind POST to transaction.then(settingsController::settings),
"/export" bind POST to settingsController::export,
)
)
}
}

View File

@ -3,11 +3,13 @@ package be.simplenotes.app.utils
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import kotlinx.serialization.json.jsonObject import kotlinx.serialization.json.jsonObject
import kotlinx.serialization.json.jsonPrimitive import kotlinx.serialization.json.jsonPrimitive
import javax.inject.Singleton
interface StaticFileResolver { interface StaticFileResolver {
fun resolve(name: String): String? fun resolve(name: String): String?
} }
@Singleton
class StaticFileResolverImpl(json: Json) : StaticFileResolver { class StaticFileResolverImpl(json: Json) : StaticFileResolver {
private val mappings: Map<String, String> private val mappings: Map<String, String>

View File

@ -13,4 +13,5 @@
<logger name="me.liuwj.ktorm.database" level="INFO"/> <logger name="me.liuwj.ktorm.database" level="INFO"/>
<logger name="com.zaxxer.hikari" level="INFO"/> <logger name="com.zaxxer.hikari" level="INFO"/>
<logger name="org.flywaydb.core" level="INFO"/> <logger name="org.flywaydb.core" level="INFO"/>
<logger name="io.micronaut" level="INFO"/>
</configuration> </configuration>

View File

@ -1,15 +1,23 @@
package be.simplenotes.app.filters package be.simplenotes.app.filters
import be.simplenotes.app.filters.auth.OptionalAuthFilter
import be.simplenotes.app.filters.auth.OptionalAuthLens
import be.simplenotes.app.filters.auth.RequiredAuthFilter
import be.simplenotes.app.filters.auth.RequiredAuthLens
import be.simplenotes.config.JwtConfig import be.simplenotes.config.JwtConfig
import be.simplenotes.domain.security.JwtPayloadExtractor
import be.simplenotes.domain.security.SimpleJwt import be.simplenotes.domain.security.SimpleJwt
import be.simplenotes.types.LoggedInUser import be.simplenotes.types.LoggedInUser
import com.natpryce.hamkrest.assertion.assertThat import com.natpryce.hamkrest.assertion.assertThat
import org.http4k.core.* import io.micronaut.context.BeanContext
import io.micronaut.inject.qualifiers.Qualifiers
import org.http4k.core.Method.GET import org.http4k.core.Method.GET
import org.http4k.core.Request
import org.http4k.core.RequestContexts
import org.http4k.core.Response
import org.http4k.core.Status.Companion.FOUND import org.http4k.core.Status.Companion.FOUND
import org.http4k.core.Status.Companion.OK import org.http4k.core.Status.Companion.OK
import org.http4k.core.cookie.cookie import org.http4k.core.cookie.cookie
import org.http4k.core.then
import org.http4k.filter.ServerFilters import org.http4k.filter.ServerFilters
import org.http4k.hamkrest.hasBody import org.http4k.hamkrest.hasBody
import org.http4k.hamkrest.hasHeader import org.http4k.hamkrest.hasHeader
@ -20,22 +28,36 @@ import org.junit.jupiter.api.Nested
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
internal class AuthFilterTest { internal class RequiredAuthFilterTest {
// region setup // region setup
private val jwtConfig = JwtConfig("secret", 1, TimeUnit.HOURS) private val jwtConfig = JwtConfig("secret", 1, TimeUnit.HOURS)
private val simpleJwt = SimpleJwt(jwtConfig) private val simpleJwt = SimpleJwt(jwtConfig)
private val extractor = JwtPayloadExtractor(simpleJwt)
private val ctx = RequestContexts()
private val requiredAuth = AuthFilter(extractor, AuthType.Required, ctx)
private val optionalAuth = AuthFilter(extractor, AuthType.Optional, ctx)
private val echoJwtPayloadHandler = { request: Request -> Response(OK).body(request.jwtPayload(ctx).toString()) } private val beanCtx = BeanContext.build()
.registerSingleton(jwtConfig)
.start()
private inline fun <reified T> BeanContext.getBean(): T = getBean(T::class.java)
private inline fun <reified T> BeanContext.getBean(name: String): T =
getBean(T::class.java, Qualifiers.byName(name))
private val requiredAuth = beanCtx.getBean<RequiredAuthFilter>()
private val requiredLens = beanCtx.getBean<RequiredAuthLens>("required")
private val optionalAuth = beanCtx.getBean<OptionalAuthFilter>()
private val optionalLens = beanCtx.getBean<OptionalAuthLens>("optional")
private val ctx = beanCtx.getBean<RequestContexts>()
private val app = ServerFilters.InitialiseRequestContext(ctx).then( private val app = ServerFilters.InitialiseRequestContext(ctx).then(
routes( routes(
"/optional" bind GET to optionalAuth.then(echoJwtPayloadHandler), "/optional" bind GET to optionalAuth.then { request: Request ->
"/protected" bind GET to requiredAuth.then(echoJwtPayloadHandler) Response(OK).body(optionalLens(request).toString())
},
"/protected" bind GET to requiredAuth.then { request: Request ->
Response(OK).body(requiredLens(request).toString())
}
) )
) )
// endregion // endregion

View File

@ -2,8 +2,10 @@ import be.simplenotes.Libs
plugins { plugins {
id("be.simplenotes.base") id("be.simplenotes.base")
kotlin("kapt")
} }
dependencies { dependencies {
implementation(Libs.koinCore) implementation(Libs.micronaut)
kapt(Libs.micronautProcessor)
} }

View File

@ -2,7 +2,9 @@ package be.simplenotes.config
import java.util.* import java.util.*
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
import javax.inject.Singleton
@Singleton
class ConfigLoader { class ConfigLoader {
//region Config loading //region Config loading
private val properties: Properties = javaClass private val properties: Properties = javaClass

View File

@ -1,10 +1,17 @@
package be.simplenotes.config package be.simplenotes.config
import org.koin.dsl.module import io.micronaut.context.annotation.Factory
import javax.inject.Singleton
val configModule = module { @Factory
single { ConfigLoader() } class ConfigModule {
single { get<ConfigLoader>().dataSourceConfig }
single { get<ConfigLoader>().jwtConfig } @Singleton
single { get<ConfigLoader>().serverConfig } internal fun dataSourceConfig(configLoader: ConfigLoader) = configLoader.dataSourceConfig
@Singleton
internal fun jwtConfig(configLoader: ConfigLoader) = configLoader.jwtConfig
@Singleton
internal fun serverConfig(configLoader: ConfigLoader) = configLoader.serverConfig
} }

View File

@ -3,6 +3,7 @@ import be.simplenotes.Libs
plugins { plugins {
id("be.simplenotes.base") id("be.simplenotes.base")
id("be.simplenotes.kotlinx-serialization") id("be.simplenotes.kotlinx-serialization")
kotlin("kapt")
} }
dependencies { dependencies {
@ -11,8 +12,10 @@ dependencies {
implementation(project(":simplenotes-persistance")) implementation(project(":simplenotes-persistance"))
implementation(project(":simplenotes-search")) implementation(project(":simplenotes-search"))
implementation(Libs.micronaut)
kapt(Libs.micronautProcessor)
implementation(Libs.kotlinxSerializationJson) implementation(Libs.kotlinxSerializationJson)
implementation(Libs.koinCore)
implementation(Libs.arrowCoreData) implementation(Libs.arrowCoreData)
implementation(Libs.konform) implementation(Libs.konform)
implementation(Libs.jbcrypt) implementation(Libs.jbcrypt)

View File

@ -1,37 +0,0 @@
package be.simplenotes.domain
import be.simplenotes.domain.security.BcryptPasswordHash
import be.simplenotes.domain.security.JwtPayloadExtractor
import be.simplenotes.domain.security.PasswordHash
import be.simplenotes.domain.security.SimpleJwt
import be.simplenotes.domain.usecases.NoteService
import be.simplenotes.domain.usecases.UserService
import be.simplenotes.domain.usecases.export.ExportUseCase
import be.simplenotes.domain.usecases.export.ExportUseCaseImpl
import be.simplenotes.domain.usecases.markdown.MarkdownConverter
import be.simplenotes.domain.usecases.markdown.MarkdownConverterImpl
import be.simplenotes.domain.usecases.users.delete.DeleteUseCase
import be.simplenotes.domain.usecases.users.delete.DeleteUseCaseImpl
import be.simplenotes.domain.usecases.users.login.LoginUseCase
import be.simplenotes.domain.usecases.users.login.LoginUseCaseImpl
import be.simplenotes.domain.usecases.users.register.RegisterUseCase
import be.simplenotes.domain.usecases.users.register.RegisterUseCaseImpl
import org.koin.dsl.module
val domainModule = module {
single<LoginUseCase> { LoginUseCaseImpl(get(), get(), get()) }
single<RegisterUseCase> { RegisterUseCaseImpl(get(), get()) }
single<DeleteUseCase> { DeleteUseCaseImpl(get(), get(), get()) }
single { UserService(get(), get(), get(), get()) }
single<PasswordHash> { BcryptPasswordHash() }
single { SimpleJwt(get()) }
single { JwtPayloadExtractor(get()) }
single {
NoteService(get(), get(), get(), get()).apply {
dropAllIndexes()
indexAll()
}
}
single<MarkdownConverter> { MarkdownConverterImpl() }
single<ExportUseCase> { ExportUseCaseImpl(get(), get()) }
}

View File

@ -2,7 +2,9 @@ package be.simplenotes.domain.security
import be.simplenotes.types.LoggedInUser import be.simplenotes.types.LoggedInUser
import com.auth0.jwt.exceptions.JWTVerificationException import com.auth0.jwt.exceptions.JWTVerificationException
import javax.inject.Singleton
@Singleton
class JwtPayloadExtractor(private val jwt: SimpleJwt) { class JwtPayloadExtractor(private val jwt: SimpleJwt) {
operator fun invoke(token: String): LoggedInUser? = try { operator fun invoke(token: String): LoggedInUser? = try {
val decodedJWT = jwt.verifier.verify(token) val decodedJWT = jwt.verifier.verify(token)

View File

@ -1,13 +1,19 @@
package be.simplenotes.domain.security package be.simplenotes.domain.security
import org.mindrot.jbcrypt.BCrypt import org.mindrot.jbcrypt.BCrypt
import javax.inject.Inject
import javax.inject.Singleton
internal interface PasswordHash { internal interface PasswordHash {
fun crypt(password: String): String fun crypt(password: String): String
fun verify(password: String, hashedPassword: String): Boolean fun verify(password: String, hashedPassword: String): Boolean
} }
internal class BcryptPasswordHash(test: Boolean = false) : PasswordHash { @Singleton
internal class BcryptPasswordHash constructor(test: Boolean) : PasswordHash {
@Inject
constructor() : this(false)
private val rounds = if (test) 4 else 10 private val rounds = if (test) 4 else 10
override fun crypt(password: String) = BCrypt.hashpw(password, BCrypt.gensalt(rounds))!! override fun crypt(password: String) = BCrypt.hashpw(password, BCrypt.gensalt(rounds))!!
override fun verify(password: String, hashedPassword: String) = BCrypt.checkpw(password, hashedPassword) override fun verify(password: String, hashedPassword: String) = BCrypt.checkpw(password, hashedPassword)

View File

@ -7,10 +7,12 @@ import com.auth0.jwt.JWTVerifier
import com.auth0.jwt.algorithms.Algorithm import com.auth0.jwt.algorithms.Algorithm
import java.util.* import java.util.*
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
import javax.inject.Singleton
internal const val userIdField = "i" internal const val userIdField = "i"
internal const val usernameField = "u" internal const val usernameField = "u"
@Singleton
class SimpleJwt(jwtConfig: JwtConfig) { class SimpleJwt(jwtConfig: JwtConfig) {
private val validityInMs = TimeUnit.MILLISECONDS.convert(jwtConfig.validity, jwtConfig.timeUnit) private val validityInMs = TimeUnit.MILLISECONDS.convert(jwtConfig.validity, jwtConfig.timeUnit)
private val algorithm = Algorithm.HMAC256(jwtConfig.secret) private val algorithm = Algorithm.HMAC256(jwtConfig.secret)

View File

@ -12,7 +12,9 @@ import be.simplenotes.types.Note
import be.simplenotes.types.PersistedNote import be.simplenotes.types.PersistedNote
import be.simplenotes.types.PersistedNoteMetadata import be.simplenotes.types.PersistedNoteMetadata
import java.util.* import java.util.*
import javax.inject.Singleton
@Singleton
class NoteService( class NoteService(
private val markdownConverter: MarkdownConverter, private val markdownConverter: MarkdownConverter,
private val noteRepository: NoteRepository, private val noteRepository: NoteRepository,

View File

@ -4,7 +4,9 @@ import be.simplenotes.domain.usecases.export.ExportUseCase
import be.simplenotes.domain.usecases.users.delete.DeleteUseCase import be.simplenotes.domain.usecases.users.delete.DeleteUseCase
import be.simplenotes.domain.usecases.users.login.LoginUseCase import be.simplenotes.domain.usecases.users.login.LoginUseCase
import be.simplenotes.domain.usecases.users.register.RegisterUseCase import be.simplenotes.domain.usecases.users.register.RegisterUseCase
import javax.inject.Singleton
@Singleton
class UserService( class UserService(
loginUseCase: LoginUseCase, loginUseCase: LoginUseCase,
registerUseCase: RegisterUseCase, registerUseCase: RegisterUseCase,

View File

@ -2,6 +2,7 @@ package be.simplenotes.domain.usecases.export
import be.simplenotes.persistance.repositories.NoteRepository import be.simplenotes.persistance.repositories.NoteRepository
import be.simplenotes.types.ExportedNote import be.simplenotes.types.ExportedNote
import io.micronaut.context.annotation.Primary
import kotlinx.serialization.builtins.ListSerializer import kotlinx.serialization.builtins.ListSerializer
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import org.apache.commons.compress.archivers.zip.ZipArchiveEntry import org.apache.commons.compress.archivers.zip.ZipArchiveEntry
@ -9,8 +10,14 @@ import org.apache.commons.compress.archivers.zip.ZipArchiveOutputStream
import java.io.ByteArrayInputStream import java.io.ByteArrayInputStream
import java.io.ByteArrayOutputStream import java.io.ByteArrayOutputStream
import java.io.InputStream import java.io.InputStream
import javax.inject.Singleton
internal class ExportUseCaseImpl(private val noteRepository: NoteRepository, private val json: Json) : ExportUseCase { @Primary
@Singleton
internal class ExportUseCaseImpl(
private val noteRepository: NoteRepository,
private val json: Json,
) : ExportUseCase {
override fun exportAsJson(userId: Int): String { override fun exportAsJson(userId: Int): String {
val notes = noteRepository.export(userId) val notes = noteRepository.export(userId)
return json.encodeToString(ListSerializer(ExportedNote.serializer()), notes) return json.encodeToString(ListSerializer(ExportedNote.serializer()), notes)

View File

@ -14,6 +14,7 @@ import io.konform.validation.ValidationErrors
import org.yaml.snakeyaml.Yaml import org.yaml.snakeyaml.Yaml
import org.yaml.snakeyaml.parser.ParserException import org.yaml.snakeyaml.parser.ParserException
import org.yaml.snakeyaml.scanner.ScannerException import org.yaml.snakeyaml.scanner.ScannerException
import javax.inject.Singleton
sealed class MarkdownParsingError sealed class MarkdownParsingError
object MissingMeta : MarkdownParsingError() object MissingMeta : MarkdownParsingError()
@ -28,6 +29,7 @@ interface MarkdownConverter {
fun renderDocument(input: String): Either<MarkdownParsingError, Document> fun renderDocument(input: String): Either<MarkdownParsingError, Document>
} }
@Singleton
internal class MarkdownConverterImpl : MarkdownConverter { internal class MarkdownConverterImpl : MarkdownConverter {
private val yamlBoundPattern = "-{3}".toRegex() private val yamlBoundPattern = "-{3}".toRegex()
private fun splitMetaFromDocument(input: String): Either<MissingMeta, MetaMdPair> { private fun splitMetaFromDocument(input: String): Either<MissingMeta, MetaMdPair> {

View File

@ -7,7 +7,11 @@ import be.simplenotes.domain.security.PasswordHash
import be.simplenotes.domain.validation.UserValidations import be.simplenotes.domain.validation.UserValidations
import be.simplenotes.persistance.repositories.UserRepository import be.simplenotes.persistance.repositories.UserRepository
import be.simplenotes.search.NoteSearcher import be.simplenotes.search.NoteSearcher
import io.micronaut.context.annotation.Primary
import javax.inject.Singleton
@Primary
@Singleton
internal class DeleteUseCaseImpl( internal class DeleteUseCaseImpl(
private val userRepository: UserRepository, private val userRepository: UserRepository,
private val passwordHash: PasswordHash, private val passwordHash: PasswordHash,

View File

@ -8,7 +8,11 @@ import be.simplenotes.domain.security.SimpleJwt
import be.simplenotes.domain.validation.UserValidations import be.simplenotes.domain.validation.UserValidations
import be.simplenotes.persistance.repositories.UserRepository import be.simplenotes.persistance.repositories.UserRepository
import be.simplenotes.types.LoggedInUser import be.simplenotes.types.LoggedInUser
import io.micronaut.context.annotation.Primary
import javax.inject.Singleton
@Singleton
@Primary
internal class LoginUseCaseImpl( internal class LoginUseCaseImpl(
private val userRepository: UserRepository, private val userRepository: UserRepository,
private val passwordHash: PasswordHash, private val passwordHash: PasswordHash,

View File

@ -7,7 +7,11 @@ import be.simplenotes.domain.security.PasswordHash
import be.simplenotes.domain.validation.UserValidations import be.simplenotes.domain.validation.UserValidations
import be.simplenotes.persistance.repositories.UserRepository import be.simplenotes.persistance.repositories.UserRepository
import be.simplenotes.types.PersistedUser import be.simplenotes.types.PersistedUser
import io.micronaut.context.annotation.Primary
import javax.inject.Singleton
@Primary
@Singleton
internal class RegisterUseCaseImpl( internal class RegisterUseCaseImpl(
private val userRepository: UserRepository, private val userRepository: UserRepository,
private val passwordHash: PasswordHash private val passwordHash: PasswordHash

View File

@ -10,8 +10,6 @@ dependencies {
implementation(project(":simplenotes-types")) implementation(project(":simplenotes-types"))
implementation(project(":simplenotes-config")) implementation(project(":simplenotes-config"))
implementation(Libs.mapstruct)
implementation(Libs.koinCore)
implementation(Libs.mariadbClient) implementation(Libs.mariadbClient)
implementation(Libs.h2) implementation(Libs.h2)
implementation(Libs.flywayCore) implementation(Libs.flywayCore)
@ -19,19 +17,36 @@ dependencies {
implementation(Libs.ktormCore) implementation(Libs.ktormCore)
implementation(Libs.ktormMysql) implementation(Libs.ktormMysql)
implementation(Libs.mapstruct)
kapt(Libs.mapstructProcessor) kapt(Libs.mapstructProcessor)
implementation(Libs.micronaut)
kapt(Libs.micronautProcessor)
testImplementation(Libs.micronaut)
kaptTest(Libs.micronautProcessor)
testImplementation(Libs.junit) testImplementation(Libs.junit)
testImplementation(Libs.assertJ) testImplementation(Libs.assertJ)
testImplementation(Libs.logbackClassic) testImplementation(Libs.logbackClassic)
testImplementation("org.testcontainers:mariadb:1.15.0-rc2") testImplementation(Libs.mariaTestContainer)
testFixturesImplementation(project(":simplenotes-types")) testFixturesImplementation(project(":simplenotes-types"))
testFixturesImplementation(project(":simplenotes-config")) testFixturesImplementation(project(":simplenotes-config"))
testFixturesImplementation("com.github.javafaker:javafaker:1.0.2") testFixturesImplementation(project(":simplenotes-persistance"))
testFixturesImplementation("org.testcontainers:mariadb:1.15.0-rc2")
testFixturesImplementation(Libs.koinCore) testFixturesImplementation(Libs.micronaut)
kaptTestFixtures(Libs.micronautProcessor)
testFixturesImplementation(Libs.faker) {
exclude(group = "org.yaml")
}
testFixturesImplementation(Libs.snakeyaml)
testFixturesImplementation(Libs.mariaTestContainer)
testFixturesImplementation(Libs.flywayCore) testFixturesImplementation(Libs.flywayCore)
testFixturesImplementation(Libs.junit) testFixturesImplementation(Libs.junit)
testFixturesImplementation(Libs.ktormCore)
testFixturesImplementation(Libs.hikariCP)
} }

View File

@ -7,11 +7,13 @@ import me.liuwj.ktorm.database.Database
import me.liuwj.ktorm.database.asIterable import me.liuwj.ktorm.database.asIterable
import me.liuwj.ktorm.database.use import me.liuwj.ktorm.database.use
import java.sql.SQLTransientException import java.sql.SQLTransientException
import javax.inject.Singleton
interface DbHealthCheck { interface DbHealthCheck {
fun isOk(): Boolean fun isOk(): Boolean
} }
@Singleton
internal class DbHealthCheckImpl( internal class DbHealthCheckImpl(
private val db: Database, private val db: Database,
private val dataSourceConfig: DataSourceConfig, private val dataSourceConfig: DataSourceConfig,

View File

@ -4,12 +4,14 @@ import be.simplenotes.config.DataSourceConfig
import be.simplenotes.persistance.utils.DbType import be.simplenotes.persistance.utils.DbType
import be.simplenotes.persistance.utils.type import be.simplenotes.persistance.utils.type
import org.flywaydb.core.Flyway import org.flywaydb.core.Flyway
import javax.inject.Singleton
import javax.sql.DataSource import javax.sql.DataSource
interface DbMigrations { interface DbMigrations {
fun migrate() fun migrate()
} }
@Singleton
internal class DbMigrationsImpl( internal class DbMigrationsImpl(
private val dataSource: DataSource, private val dataSource: DataSource,
private val dataSourceConfig: DataSourceConfig, private val dataSourceConfig: DataSourceConfig,

View File

@ -2,46 +2,42 @@ package be.simplenotes.persistance
import be.simplenotes.config.DataSourceConfig import be.simplenotes.config.DataSourceConfig
import be.simplenotes.persistance.converters.NoteConverter import be.simplenotes.persistance.converters.NoteConverter
import be.simplenotes.persistance.converters.NoteConverterImpl
import be.simplenotes.persistance.converters.UserConverter import be.simplenotes.persistance.converters.UserConverter
import be.simplenotes.persistance.converters.UserConverterImpl
import be.simplenotes.persistance.notes.NoteRepositoryImpl
import be.simplenotes.persistance.repositories.NoteRepository
import be.simplenotes.persistance.repositories.UserRepository
import be.simplenotes.persistance.users.UserRepositoryImpl
import com.zaxxer.hikari.HikariConfig import com.zaxxer.hikari.HikariConfig
import com.zaxxer.hikari.HikariDataSource import com.zaxxer.hikari.HikariDataSource
import io.micronaut.context.annotation.Bean
import io.micronaut.context.annotation.Factory
import me.liuwj.ktorm.database.Database import me.liuwj.ktorm.database.Database
import org.koin.dsl.bind import org.mapstruct.factory.Mappers
import org.koin.dsl.module import javax.inject.Singleton
import org.koin.dsl.onClose
import javax.sql.DataSource import javax.sql.DataSource
private fun hikariDataSource(conf: DataSourceConfig): HikariDataSource { @Factory
val hikariConfig = HikariConfig().also { class PersistanceModule {
it.jdbcUrl = conf.jdbcUrl
it.driverClassName = conf.driverClassName
it.username = conf.username
it.password = conf.password
it.maximumPoolSize = conf.maximumPoolSize
it.connectionTimeout = conf.connectionTimeout
}
return HikariDataSource(hikariConfig)
}
val migrationModule = module { @Singleton
single<DbMigrations> { DbMigrationsImpl(get(), get()) } internal fun noteConverter() = Mappers.getMapper(NoteConverter::class.java)
}
val persistanceModule = module { @Singleton
single<NoteConverter> { NoteConverterImpl() } internal fun userConverter() = Mappers.getMapper(UserConverter::class.java)
single<UserConverter> { UserConverterImpl() }
single<UserRepository> { UserRepositoryImpl(get(), get()) } @Singleton
single<NoteRepository> { NoteRepositoryImpl(get(), get()) } internal fun database(migrations: DbMigrations, dataSource: DataSource): Database {
single { hikariDataSource(get()) } bind DataSource::class onClose { it?.close() } migrations.migrate()
single { return Database.connect(dataSource)
get<DbMigrations>().migrate() }
Database.connect(get<DataSource>())
@Singleton
@Bean(preDestroy = "close")
internal fun dataSource(conf: DataSourceConfig): HikariDataSource {
val hikariConfig = HikariConfig().also {
it.jdbcUrl = conf.jdbcUrl
it.driverClassName = conf.driverClassName
it.username = conf.username
it.password = conf.password
it.maximumPoolSize = conf.maximumPoolSize
it.connectionTimeout = conf.connectionTimeout
}
return HikariDataSource(hikariConfig)
} }
single<DbHealthCheck> { DbHealthCheckImpl(get(), get()) }
} }

View File

@ -11,9 +11,14 @@ import me.liuwj.ktorm.dsl.*
import me.liuwj.ktorm.entity.* import me.liuwj.ktorm.entity.*
import java.time.LocalDateTime import java.time.LocalDateTime
import java.util.* import java.util.*
import javax.inject.Singleton
import kotlin.collections.HashMap import kotlin.collections.HashMap
internal class NoteRepositoryImpl(private val db: Database, private val converter: NoteConverter) : NoteRepository { @Singleton
internal class NoteRepositoryImpl(
private val db: Database,
private val converter: NoteConverter,
) : NoteRepository {
@Throws(IllegalArgumentException::class) @Throws(IllegalArgumentException::class)
override fun findAll( override fun findAll(

View File

@ -9,8 +9,13 @@ import me.liuwj.ktorm.dsl.*
import me.liuwj.ktorm.entity.any import me.liuwj.ktorm.entity.any
import me.liuwj.ktorm.entity.find import me.liuwj.ktorm.entity.find
import java.sql.SQLIntegrityConstraintViolationException import java.sql.SQLIntegrityConstraintViolationException
import javax.inject.Singleton
internal class UserRepositoryImpl(private val db: Database, private val converter: UserConverter) : UserRepository { @Singleton
internal class UserRepositoryImpl(
private val db: Database,
private val converter: UserConverter,
) : UserRepository {
override fun create(user: User): PersistedUser? { override fun create(user: User): PersistedUser? {
return try { return try {
val id = db.insertAndGenerateKey(Users) { val id = db.insertAndGenerateKey(Users) {

View File

@ -7,19 +7,16 @@ import org.junit.jupiter.api.parallel.ResourceLock
@ResourceLock("h2") @ResourceLock("h2")
class H2DbHealthCheckImplTest : DbTest() { class H2DbHealthCheckImplTest : DbTest() {
private val healthCheck = koin.get<DbHealthCheck>()
override fun dataSourceConfig() = h2dataSourceConfig() override fun dataSourceConfig() = h2dataSourceConfig()
@Test @Test
fun healthCheck() { fun healthCheck() {
assertThat(healthCheck.isOk()).isTrue assertThat(beanContext.getBean<DbHealthCheck>().isOk()).isTrue
} }
} }
@ResourceLock("mariadb") @ResourceLock("mariadb")
class MariaDbHealthCheckImplTest : DbTest() { class MariaDbHealthCheckImplTest : DbTest() {
private val healthCheck = koin.get<DbHealthCheck>()
lateinit var mariaDB: KMariadbContainer lateinit var mariaDB: KMariadbContainer
override fun dataSourceConfig(): DataSourceConfig { override fun dataSourceConfig(): DataSourceConfig {
@ -30,9 +27,9 @@ class MariaDbHealthCheckImplTest : DbTest() {
@Test @Test
fun healthCheck() { fun healthCheck() {
val healthCheck = beanContext.getBean<DbHealthCheck>()
assertThat(healthCheck.isOk()).isTrue assertThat(healthCheck.isOk()).isTrue
mariaDB.stop() mariaDB.stop()
assertThat(healthCheck.isOk()).isFalse assertThat(healthCheck.isOk()).isFalse
} }
} }

View File

@ -0,0 +1,44 @@
package be.simplenotes.persistance
import be.simplenotes.config.DataSourceConfig
import io.micronaut.context.BeanContext
import org.flywaydb.core.Flyway
import org.junit.jupiter.api.AfterAll
import org.junit.jupiter.api.BeforeAll
import org.junit.jupiter.api.BeforeEach
import javax.sql.DataSource
abstract class DbTest {
abstract fun dataSourceConfig(): DataSourceConfig
val beanContext = BeanContext
.build()
inline fun <reified T> BeanContext.getBean(): T = getBean(T::class.java)
@BeforeAll
fun setComponent() {
beanContext
.registerSingleton(dataSourceConfig())
.start()
}
@BeforeEach
fun beforeEach() {
val migration = beanContext.getBean<DbMigrations>()
val dataSource = beanContext.getBean<DataSource>()
Flyway.configure()
.dataSource(dataSource)
.load()
.clean()
migration.migrate()
}
@AfterAll
fun closeCtx() {
beanContext.close()
}
}

View File

@ -21,15 +21,18 @@ import java.sql.SQLIntegrityConstraintViolationException
internal abstract class BaseNoteRepositoryImplTest : DbTest() { internal abstract class BaseNoteRepositoryImplTest : DbTest() {
private val noteRepo = koin.get<NoteRepository>() private lateinit var noteRepo: NoteRepository
private val userRepo = koin.get<UserRepository>() private lateinit var userRepo: UserRepository
private val db = koin.get<Database>() private lateinit var db: Database
private lateinit var user1: PersistedUser private lateinit var user1: PersistedUser
private lateinit var user2: PersistedUser private lateinit var user2: PersistedUser
@BeforeEach @BeforeEach
fun insertUsers() { fun insertUsers() {
noteRepo = beanContext.getBean()
userRepo = beanContext.getBean()
db = beanContext.getBean()
user1 = userRepo.createFakeUser()!! user1 = userRepo.createFakeUser()!!
user2 = userRepo.createFakeUser()!! user2 = userRepo.createFakeUser()!!
} }

View File

@ -7,13 +7,20 @@ import me.liuwj.ktorm.dsl.eq
import me.liuwj.ktorm.entity.find import me.liuwj.ktorm.entity.find
import me.liuwj.ktorm.entity.toList import me.liuwj.ktorm.entity.toList
import org.assertj.core.api.Assertions.assertThat import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Nested import org.junit.jupiter.api.Nested
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
internal abstract class BaseUserRepositoryImplTest : DbTest() { internal abstract class BaseUserRepositoryImplTest : DbTest() {
private val userRepo = koin.get<UserRepository>() private lateinit var userRepo: UserRepository
private val db = koin.get<Database>() private lateinit var db: Database
@BeforeEach
fun setup() {
userRepo = beanContext.getBean()
db = beanContext.getBean()
}
@Test @Test
fun `insert user`() { fun `insert user`() {

View File

@ -1,40 +1,35 @@
package be.simplenotes.persistance package be.simplenotes.persistance
import be.simplenotes.config.DataSourceConfig import be.simplenotes.config.DataSourceConfig
import org.flywaydb.core.Flyway.* import io.micronaut.context.BeanContext
import org.junit.jupiter.api.AfterAll import org.flywaydb.core.Flyway
import org.junit.jupiter.api.BeforeAll
import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.BeforeEach
import org.koin.dsl.koinApplication
import org.koin.dsl.module
import javax.sql.DataSource import javax.sql.DataSource
abstract class DbTest { abstract class DbTest {
abstract fun dataSourceConfig(): DataSourceConfig abstract fun dataSourceConfig(): DataSourceConfig
private val testModule = module { val beanContext = BeanContext.build()
single { dataSourceConfig() }
inline fun <reified T> BeanContext.getBean(): T = getBean(T::class.java)
@BeforeAll
fun setComponent() {
beanContext.registerSingleton(dataSourceConfig())
} }
private val koinApp = koinApplication {
modules(persistanceModule, migrationModule, testModule)
}
val koin = koinApp.koin
@AfterAll
fun afterAll() = koinApp.close()
@BeforeEach @BeforeEach
fun beforeEach() { fun beforeEach() {
val migration = koin.get<DbMigrations>() val migration = beanContext.getBean<DbMigrations>()
val dataSource = koin.get<DataSource>() val dataSource = beanContext.getBean<DataSource>()
configure()
Flyway.configure()
.dataSource(dataSource) .dataSource(dataSource)
.load() .load()
.clean() .clean()
migration.migrate() migration.migrate()
} }
} }

View File

@ -2,6 +2,7 @@ import be.simplenotes.Libs
plugins { plugins {
id("be.simplenotes.base") id("be.simplenotes.base")
kotlin("kapt")
} }
dependencies { dependencies {
@ -11,7 +12,9 @@ dependencies {
implementation(Libs.luceneQueryParser) implementation(Libs.luceneQueryParser)
implementation(Libs.luceneAnalyzersCommon) implementation(Libs.luceneAnalyzersCommon)
implementation(Libs.slf4jApi) implementation(Libs.slf4jApi)
implementation(Libs.koinCore)
implementation(Libs.micronaut)
kapt(Libs.micronautProcessor)
testImplementation(Libs.junit) testImplementation(Libs.junit)
testImplementation(Libs.assertJ) testImplementation(Libs.assertJ)

View File

@ -12,8 +12,17 @@ import org.slf4j.LoggerFactory
import java.io.File import java.io.File
import java.nio.file.Path import java.nio.file.Path
import java.util.* import java.util.*
import javax.inject.Named
import javax.inject.Singleton
@Singleton
internal class NoteSearcherImpl(
@Named("search-index")
basePath: Path,
) : NoteSearcher {
constructor() : this(Path.of("/tmp", "lucene"))
internal class NoteSearcherImpl(basePath: Path = Path.of("/tmp", "lucene")) : NoteSearcher {
private val baseFile = basePath.toFile() private val baseFile = basePath.toFile()
private val logger = LoggerFactory.getLogger(javaClass) private val logger = LoggerFactory.getLogger(javaClass)

View File

@ -1,8 +0,0 @@
package be.simplenotes.search
import org.koin.dsl.module
import java.nio.file.Path
val searchModule = module {
single<NoteSearcher> { NoteSearcherImpl(Path.of(".lucene")) }
}

View File

@ -0,0 +1,14 @@
package be.simplenotes.search
import io.micronaut.context.annotation.Factory
import io.micronaut.context.annotation.Prototype
import java.nio.file.Path
import javax.inject.Named
@Factory
class SearchModule {
@Named("search-index")
@Prototype
internal fun luceneIndex() = Path.of(".lucene")
}

View File

@ -2,13 +2,16 @@ import be.simplenotes.Libs
plugins { plugins {
id("be.simplenotes.base") id("be.simplenotes.base")
kotlin("kapt")
} }
dependencies { dependencies {
implementation(project(":simplenotes-types")) implementation(project(":simplenotes-types"))
implementation(Libs.koinCore)
implementation(Libs.konform) implementation(Libs.konform)
implementation(Libs.kotlinxHtml) implementation(Libs.kotlinxHtml)
implementation(Libs.prettytime) implementation(Libs.prettytime)
implementation(Libs.micronaut)
kapt(Libs.micronautProcessor)
} }

View File

@ -3,8 +3,11 @@ package be.simplenotes.views
import be.simplenotes.types.LoggedInUser import be.simplenotes.types.LoggedInUser
import kotlinx.html.* import kotlinx.html.*
import kotlinx.html.ThScope.col import kotlinx.html.ThScope.col
import javax.inject.Named
import javax.inject.Singleton
class BaseView(styles: String) : View(styles) { @Singleton
class BaseView(@Named("styles") styles: String) : View(styles) {
fun renderHome(loggedInUser: LoggedInUser?) = renderPage( fun renderHome(loggedInUser: LoggedInUser?) = renderPage(
title = "Home", title = "Home",
description = "A fast and simple note taking website", description = "A fast and simple note taking website",

View File

@ -4,8 +4,11 @@ import be.simplenotes.views.components.Alert
import be.simplenotes.views.components.alert import be.simplenotes.views.components.alert
import kotlinx.html.a import kotlinx.html.a
import kotlinx.html.div import kotlinx.html.div
import javax.inject.Named
import javax.inject.Singleton
class ErrorView(styles: String) : View(styles) { @Singleton
class ErrorView(@Named("styles") styles: String) : View(styles) {
enum class Type(val title: String) { enum class Type(val title: String) {
SqlTransientError("Database unavailable"), SqlTransientError("Database unavailable"),

View File

@ -6,8 +6,11 @@ import be.simplenotes.types.PersistedNoteMetadata
import be.simplenotes.views.components.* import be.simplenotes.views.components.*
import io.konform.validation.ValidationError import io.konform.validation.ValidationError
import kotlinx.html.* import kotlinx.html.*
import javax.inject.Named
import javax.inject.Singleton
class NoteView(styles: String) : View(styles) { @Singleton
class NoteView(@Named("styles") styles: String) : View(styles) {
fun noteEditor( fun noteEditor(
loggedInUser: LoggedInUser, loggedInUser: LoggedInUser,

View File

@ -8,8 +8,11 @@ import be.simplenotes.views.extensions.summary
import io.konform.validation.ValidationError import io.konform.validation.ValidationError
import kotlinx.html.* import kotlinx.html.*
import kotlinx.html.ButtonType.submit import kotlinx.html.ButtonType.submit
import javax.inject.Named
import javax.inject.Singleton
class SettingView(styles: String) : View(styles) { @Singleton
class SettingView(@Named("styles") styles: String) : View(styles) {
fun settings( fun settings(
loggedInUser: LoggedInUser, loggedInUser: LoggedInUser,

View File

@ -7,8 +7,11 @@ import be.simplenotes.views.components.input
import be.simplenotes.views.components.submitButton import be.simplenotes.views.components.submitButton
import io.konform.validation.ValidationError import io.konform.validation.ValidationError
import kotlinx.html.* import kotlinx.html.*
import javax.inject.Named
import javax.inject.Singleton
class UserView(styles: String) : View(styles) { @Singleton
class UserView(@Named("styles") styles: String) : View(styles) {
fun register( fun register(
loggedInUser: LoggedInUser?, loggedInUser: LoggedInUser?,
error: String? = null, error: String? = null,

View File

@ -1,12 +0,0 @@
package be.simplenotes.views
import org.koin.core.qualifier.named
import org.koin.dsl.module
val viewModule = module {
single { ErrorView(get(named("styles"))) }
single { UserView(get(named("styles"))) }
single { BaseView(get(named("styles"))) }
single { SettingView(get(named("styles"))) }
single { NoteView(get(named("styles"))) }
}