12 Commits

Author SHA1 Message Date
hubert 195c7c10ac Import feature 2023-05-09 22:46:07 +02:00
hubert 724fa0483e Tailwind 3 2023-05-09 22:40:39 +02:00
hubert f2bdc8d6c7 Update kotlin libs 2023-05-09 22:40:10 +02:00
hubert 5aa2e80c5f JDK 19 + Gradle 8.1.1 + kotlin 1.8.21 + spotless 2023-05-09 22:39:34 +02:00
hubert 235e8b6e3c Use gradle catalogs 2021-05-06 12:22:16 +02:00
hubert 1e0fe12396 Upgrade kotlin, kotlinx-html, gradle, shadow 2021-05-06 12:22:16 +02:00
hubert 7ad8b7039b Update config 2021-04-12 21:15:42 +02:00
hubert 204ae7988e Update ktorm, clean repositories and drop mariadb support 2021-04-12 18:35:21 +02:00
hubert a4bf998c5b Remove Boilerplate Use-case thingy 2021-03-03 16:57:53 +01:00
hubert 3e1683dfe5 Use a proper search input parser 2021-03-03 14:28:34 +01:00
hubert 51b682c593 Skip slow tests by default 2021-03-03 14:28:34 +01:00
hubert ea110d51d3 Simplify configuration 2021-02-27 20:34:44 +01:00
144 changed files with 2252 additions and 3475 deletions
+3 -2
View File
@@ -13,5 +13,6 @@ insert_final_newline = true
indent_size = 4
insert_final_newline = true
max_line_length = 120
disabled_rules = no-wildcard-imports
kotlin_imports_layout = idea
ktlint_standard_no-wildcard-imports = disabled
ktlint_standard_import-ordering = disabled
ktlint_standard_multiline-if-else = disabled
-5
View File
@@ -1,6 +1 @@
# mariadb
MYSQL_ROOT_PASSWORD=
MYSQL_PASSWORD=
# simplenotes
DB_PASSWORD=
JWT_SECRET=
+7 -8
View File
@@ -1,8 +1,8 @@
FROM openjdk:15-alpine as jdkbuilder
FROM eclipse-temurin:19-alpine as jdkbuilder
RUN apk add --no-cache binutils
ENV MODULES java.base,java.compiler,java.desktop,java.instrument,java.logging,java.management,java.naming,java.scripting,java.security.jgss,java.sql,java.sql.rowset,java.transaction.xa,java.xml,jdk.net
ENV MODULES java.base,java.compiler,java.desktop,java.instrument,java.logging,java.management,java.naming,java.scripting,java.security.jgss,java.sql,java.sql.rowset,java.transaction.xa,java.xml,jdk.net,jdk.zipfs
RUN jlink --output /myjdk --module-path $JAVA_HOME/jmods --add-modules $MODULES --no-header-files --no-man-pages --strip-debug --compress=2
@@ -12,18 +12,17 @@ FROM alpine
RUN apk add --no-cache curl
ENV APPLICATION_USER simplenotes
RUN adduser -D -g '' $APPLICATION_USER
RUN mkdir /app
RUN chown -R $APPLICATION_USER /app
USER $APPLICATION_USER
RUN mkdir /app/data
COPY --from=jdkbuilder /myjdk /myjdk
COPY app/build/libs/app-with-dependencies*.jar /app/simplenotes.jar
WORKDIR /app
VOLUME /app/data
ENV SERVER_HOST 0.0.0.0
CMD [ \
"/myjdk/bin/java", \
"--add-opens", \
+8 -11
View File
@@ -1,4 +1,3 @@
import be.simplenotes.Libs
import be.simplenotes.micronaut
plugins {
@@ -14,20 +13,18 @@ dependencies {
implementation(project(":views"))
implementation(project(":css"))
implementation(Libs.Http4k.core)
implementation(Libs.Jetty.server)
implementation(Libs.Jetty.servlet)
implementation(Libs.javaxServlet)
implementation(Libs.Kotlinx.Serialization.json)
implementation(libs.http4k.core)
implementation(libs.http4k.multipart)
implementation(libs.bundles.jetty)
implementation(libs.kotlinx.serialization.json)
implementation(Libs.Slf4J.api)
runtimeOnly(Libs.Slf4J.logback)
implementation(libs.slf4j.api)
runtimeOnly(libs.slf4j.logback)
micronaut()
testImplementation(Libs.Test.junit)
testImplementation(Libs.Test.assertJ)
testImplementation(Libs.Http4k.testingHamkrest)
testImplementation(libs.bundles.test)
testImplementation(libs.http4k.testing.hamkrest)
}
docker {
+3 -3
View File
@@ -1,10 +1,10 @@
package be.simplenotes.app
import jakarta.annotation.PostConstruct
import jakarta.annotation.PreDestroy
import jakarta.inject.Singleton
import org.http4k.server.Http4kServer
import org.slf4j.LoggerFactory
import javax.annotation.PostConstruct
import javax.annotation.PreDestroy
import javax.inject.Singleton
import be.simplenotes.config.ServerConfig as SimpleNotesServerConfig
@Singleton
+5 -1
View File
@@ -4,7 +4,11 @@ import io.micronaut.context.ApplicationContext
import java.lang.Runtime.getRuntime
fun main() {
val ctx = ApplicationContext.run()
val env = if (System.getenv("ENV") == "dev") "dev" else "prod"
val ctx = ApplicationContext.builder()
.deduceEnvironment(false)
.environments(env)
.start()
ctx.createBean(Server::class.java)
getRuntime().addShutdownHook(Thread { ctx.stop() })
}
+5 -5
View File
@@ -1,7 +1,7 @@
package be.simplenotes.app.api
import be.simplenotes.app.extensions.auto
import be.simplenotes.domain.usecases.NoteService
import be.simplenotes.domain.NoteService
import be.simplenotes.types.LoggedInUser
import be.simplenotes.types.PersistedNote
import be.simplenotes.types.PersistedNoteMetadata
@@ -16,7 +16,7 @@ import org.http4k.core.Status.Companion.OK
import org.http4k.lens.Path
import org.http4k.lens.uuid
import java.util.*
import javax.inject.Singleton
import jakarta.inject.Singleton
@Singleton
class ApiNoteController(
@@ -28,7 +28,7 @@ class ApiNoteController(
val content = noteContentLens(request)
return noteService.create(loggedInUser, content).fold(
{ Response(BAD_REQUEST) },
{ uuidContentLens(UuidContent(it.uuid), Response(OK)) }
{ uuidContentLens(UuidContent(it.uuid), Response(OK)) },
)
}
@@ -51,7 +51,7 @@ class ApiNoteController(
{
if (it == null) Response(NOT_FOUND)
else Response(OK)
}
},
)
}
@@ -76,4 +76,4 @@ data class NoteContent(val content: String)
data class UuidContent(@Contextual val uuid: UUID)
@Serializable
data class SearchContent(@Contextual val query: String)
data class SearchContent(val query: String)
+4 -4
View File
@@ -1,15 +1,15 @@
package be.simplenotes.app.api
import be.simplenotes.app.extensions.auto
import be.simplenotes.domain.usecases.UserService
import be.simplenotes.domain.usecases.users.login.LoginForm
import be.simplenotes.domain.LoginForm
import be.simplenotes.domain.UserService
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
import org.http4k.core.Request
import org.http4k.core.Response
import org.http4k.core.Status.Companion.BAD_REQUEST
import org.http4k.core.Status.Companion.OK
import javax.inject.Singleton
import jakarta.inject.Singleton
@Singleton
class ApiUserController(
@@ -23,7 +23,7 @@ class ApiUserController(
.login(loginFormLens(request))
.fold(
{ Response(BAD_REQUEST) },
{ tokenLens(Token(it), Response(OK)) }
{ tokenLens(Token(it), Response(OK)) },
)
}
+1 -1
View File
@@ -6,7 +6,7 @@ import be.simplenotes.views.BaseView
import org.http4k.core.Request
import org.http4k.core.Response
import org.http4k.core.Status.Companion.OK
import javax.inject.Singleton
import jakarta.inject.Singleton
@Singleton
class BaseController(private val view: BaseView) {
@@ -1,14 +0,0 @@
package be.simplenotes.app.controllers
import be.simplenotes.domain.usecases.HealthCheckService
import org.http4k.core.Request
import org.http4k.core.Response
import org.http4k.core.Status.Companion.OK
import org.http4k.core.Status.Companion.SERVICE_UNAVAILABLE
import javax.inject.Singleton
@Singleton
class HealthCheckController(private val healthCheckService: HealthCheckService) {
fun healthCheck(@Suppress("UNUSED_PARAMETER") request: Request) =
if (healthCheckService.isOk()) Response(OK) else Response(SERVICE_UNAVAILABLE)
}
+17 -19
View File
@@ -2,10 +2,8 @@ package be.simplenotes.app.controllers
import be.simplenotes.app.extensions.html
import be.simplenotes.app.extensions.redirect
import be.simplenotes.domain.usecases.NoteService
import be.simplenotes.domain.usecases.markdown.InvalidMeta
import be.simplenotes.domain.usecases.markdown.MissingMeta
import be.simplenotes.domain.usecases.markdown.ValidationError
import be.simplenotes.domain.MarkdownParsingError
import be.simplenotes.domain.NoteService
import be.simplenotes.types.LoggedInUser
import be.simplenotes.views.NoteView
import org.http4k.core.Method
@@ -17,7 +15,7 @@ import org.http4k.core.Status.Companion.OK
import org.http4k.core.body.form
import org.http4k.routing.path
import java.util.*
import javax.inject.Singleton
import jakarta.inject.Singleton
import kotlin.math.abs
@Singleton
@@ -34,27 +32,27 @@ class NoteController(
return noteService.create(loggedInUser, markdownForm).fold(
{
val html = when (it) {
MissingMeta -> view.noteEditor(
MarkdownParsingError.MissingMeta -> view.noteEditor(
loggedInUser,
error = "Missing note metadata",
textarea = markdownForm
textarea = markdownForm,
)
InvalidMeta -> view.noteEditor(
MarkdownParsingError.InvalidMeta -> view.noteEditor(
loggedInUser,
error = "Invalid note metadata",
textarea = markdownForm
textarea = markdownForm,
)
is ValidationError -> view.noteEditor(
is MarkdownParsingError.ValidationError -> view.noteEditor(
loggedInUser,
validationErrors = it.validationErrors,
textarea = markdownForm
textarea = markdownForm,
)
}
Response(BAD_REQUEST).html(html)
},
{
Response.redirect("/notes/${it.uuid}")
}
},
)
}
@@ -113,27 +111,27 @@ class NoteController(
return noteService.update(loggedInUser, note.uuid, markdownForm).fold(
{
val html = when (it) {
MissingMeta -> view.noteEditor(
MarkdownParsingError.MissingMeta -> view.noteEditor(
loggedInUser,
error = "Missing note metadata",
textarea = markdownForm
textarea = markdownForm,
)
InvalidMeta -> view.noteEditor(
MarkdownParsingError.InvalidMeta -> view.noteEditor(
loggedInUser,
error = "Invalid note metadata",
textarea = markdownForm
textarea = markdownForm,
)
is ValidationError -> view.noteEditor(
is MarkdownParsingError.ValidationError -> view.noteEditor(
loggedInUser,
validationErrors = it.validationErrors,
textarea = markdownForm
textarea = markdownForm,
)
}
Response(BAD_REQUEST).html(html)
},
{
Response.redirect("/notes/${note.uuid}")
}
},
)
}
+22 -13
View File
@@ -2,19 +2,19 @@ package be.simplenotes.app.controllers
import be.simplenotes.app.extensions.html
import be.simplenotes.app.extensions.redirect
import be.simplenotes.domain.usecases.UserService
import be.simplenotes.domain.usecases.users.delete.DeleteError
import be.simplenotes.domain.usecases.users.delete.DeleteForm
import be.simplenotes.domain.*
import be.simplenotes.types.LoggedInUser
import be.simplenotes.views.SettingView
import jakarta.inject.Singleton
import org.http4k.core.*
import org.http4k.core.body.form
import org.http4k.core.cookie.invalidateCookie
import javax.inject.Singleton
@Singleton
class SettingsController(
private val userService: UserService,
private val exportService: ExportService,
private val importService: ImportService,
private val settingView: SettingView,
) {
fun settings(request: Request, loggedInUser: LoggedInUser): Response {
@@ -31,20 +31,21 @@ class SettingsController(
DeleteError.WrongPassword -> Response(Status.OK).html(
settingView.settings(
loggedInUser,
error = "Wrong password"
)
error = "Wrong password",
),
)
is DeleteError.InvalidForm -> Response(Status.OK).html(
settingView.settings(
loggedInUser,
validationErrors = it.validationErrors
)
validationErrors = it.validationErrors,
),
)
}
},
{
Response.redirect("/").invalidateCookie("Bearer")
}
},
)
}
@@ -61,20 +62,28 @@ class SettingsController(
return if (isDownload) {
val filename = "simplenotes-export-${loggedInUser.username}"
if (request.form("format") == "zip") {
val zip = userService.exportAsZip(loggedInUser.userId)
val zip = exportService.exportAsZip(loggedInUser.userId)
Response(Status.OK)
.with(attachment("$filename.zip", "application/zip"))
.body(zip)
} else
Response(Status.OK)
.with(attachment("$filename.json", "application/json"))
.body(userService.exportAsJson(loggedInUser.userId))
} else Response(Status.OK).body(userService.exportAsJson(loggedInUser.userId)).header(
.body(exportService.exportAsJson(loggedInUser.userId))
} else Response(Status.OK).body(exportService.exportAsJson(loggedInUser.userId)).header(
"Content-Type",
"application/json"
"application/json",
)
}
private fun Request.deleteForm(loggedInUser: LoggedInUser) =
DeleteForm(loggedInUser.username, form("password"), form("checked") != null)
fun import(request: Request, loggedInUser: LoggedInUser): Response {
val form = MultipartFormBody.from(request)
val file = form.file("file") ?: return Response(Status.BAD_REQUEST)
val json = file.content.bufferedReader().readText()
importService.importJson(loggedInUser, json)
return Response.redirect("/notes")
}
}
+18 -22
View File
@@ -4,11 +4,7 @@ import be.simplenotes.app.extensions.html
import be.simplenotes.app.extensions.isSecure
import be.simplenotes.app.extensions.redirect
import be.simplenotes.config.JwtConfig
import be.simplenotes.domain.usecases.UserService
import be.simplenotes.domain.usecases.users.login.*
import be.simplenotes.domain.usecases.users.register.InvalidRegisterForm
import be.simplenotes.domain.usecases.users.register.RegisterForm
import be.simplenotes.domain.usecases.users.register.UserExists
import be.simplenotes.domain.*
import be.simplenotes.types.LoggedInUser
import be.simplenotes.views.UserView
import org.http4k.core.Method.GET
@@ -21,7 +17,7 @@ import org.http4k.core.cookie.SameSite
import org.http4k.core.cookie.cookie
import org.http4k.core.cookie.invalidateCookie
import java.util.concurrent.TimeUnit
import javax.inject.Singleton
import jakarta.inject.Singleton
@Singleton
class UserController(
@@ -31,7 +27,7 @@ class UserController(
) {
fun register(request: Request, loggedInUser: LoggedInUser?): Response {
if (request.method == GET) return Response(OK).html(
userView.register(loggedInUser)
userView.register(loggedInUser),
)
val result = userService.register(request.registerForm())
@@ -39,21 +35,21 @@ class UserController(
return result.fold(
{
val html = when (it) {
UserExists -> userView.register(
RegisterError.UserExists -> userView.register(
loggedInUser,
error = "User already exists"
error = "User already exists",
)
is InvalidRegisterForm ->
is RegisterError.InvalidRegisterForm ->
userView.register(
loggedInUser,
validationErrors = it.validationErrors
validationErrors = it.validationErrors,
)
}
Response(OK).html(html)
},
{
Response.redirect("/login")
}
},
)
}
@@ -62,7 +58,7 @@ class UserController(
fun login(request: Request, loggedInUser: LoggedInUser?): Response {
if (request.method == GET) return Response(OK).html(
userView.login(loggedInUser)
userView.login(loggedInUser),
)
val result = userService.login(request.loginForm())
@@ -70,27 +66,27 @@ class UserController(
return result.fold(
{
val html = when (it) {
Unregistered ->
LoginError.Unregistered ->
userView.login(
loggedInUser,
error = "User does not exist"
error = "User does not exist",
)
WrongPassword ->
LoginError.WrongPassword ->
userView.login(
loggedInUser,
error = "Wrong password"
error = "Wrong password",
)
is InvalidLoginForm ->
is LoginError.InvalidLoginForm ->
userView.login(
loggedInUser,
validationErrors = it.validationErrors
validationErrors = it.validationErrors,
)
}
Response(OK).html(html)
},
{ token ->
Response.redirect("/notes").loginCookie(token, request.isSecure())
}
},
)
}
@@ -105,8 +101,8 @@ class UserController(
httpOnly = true,
sameSite = SameSite.Lax,
maxAge = validityInSeconds,
secure = secure
)
secure = secure,
),
)
}
+3 -3
View File
@@ -24,13 +24,13 @@ fun Request.isSecure() = header("X-Forwarded-Proto")?.contains("https") ?: false
val bodyLens = httpBodyRoot(
listOf(Meta(true, "body", ParamMeta.ObjectParam, "body")),
ContentType.APPLICATION_JSON.withNoDirectives(),
ContentNegotiation.StrictNoDirective
ContentNegotiation.StrictNoDirective,
).map(
{ it.payload.asString() },
{ Body(it) }
{ Body(it) },
)
inline fun <reified T> Json.auto(): BiDiBodyLensSpec<T> = bodyLens.map(
{ decodeFromString(it) },
{ encodeToString(it) }
{ encodeToString(it) },
)
+1 -1
View File
@@ -10,7 +10,7 @@ import org.http4k.core.Status.Companion.NOT_IMPLEMENTED
import org.http4k.core.Status.Companion.SERVICE_UNAVAILABLE
import org.slf4j.LoggerFactory
import java.sql.SQLTransientException
import javax.inject.Singleton
import jakarta.inject.Singleton
@Singleton
class ErrorFilter(private val errorView: ErrorView) : Filter {
+2 -2
View File
@@ -8,14 +8,14 @@ import org.eclipse.jetty.servlet.ServletHolder
import org.http4k.core.HttpHandler
import org.http4k.server.Http4kServer
import org.http4k.server.ServerConfig
import org.http4k.servlet.asServlet
import org.http4k.servlet.jakarta.asServlet
class Jetty(private val port: Int, private val server: Server) : ServerConfig {
constructor(port: Int, vararg inConnectors: ConnectorBuilder) : this(
port,
Server().apply {
inConnectors.forEach { addConnector(it(this)) }
}
},
)
override fun toServer(http: HttpHandler): Http4kServer {
+3 -3
View File
@@ -7,8 +7,8 @@ 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
import jakarta.inject.Named
import jakarta.inject.Singleton
@Factory
class AuthModule {
@@ -39,7 +39,7 @@ class AuthModule {
simpleJwt = simpleJwt,
lens = lens,
source = JwtSource.Header,
redirect = false
redirect = false,
)
@Singleton
+1 -1
View File
@@ -7,7 +7,7 @@ import kotlinx.serialization.json.Json
import kotlinx.serialization.modules.SerializersModule
import java.time.LocalDateTime
import java.util.*
import javax.inject.Singleton
import jakarta.inject.Singleton
@Factory
class JsonModule {
+2 -2
View File
@@ -9,8 +9,8 @@ import io.micronaut.context.annotation.Factory
import org.eclipse.jetty.server.ServerConnector
import org.http4k.server.Http4kServer
import org.http4k.server.asServer
import javax.inject.Named
import javax.inject.Singleton
import jakarta.inject.Named
import jakarta.inject.Singleton
import org.eclipse.jetty.server.Server as JettyServer
import org.http4k.server.ServerConfig as Http4kServerConfig
+4 -5
View File
@@ -12,8 +12,8 @@ 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
import jakarta.inject.Named
import jakarta.inject.Singleton
@Singleton
class ApiRoutes(
@@ -23,7 +23,6 @@ class ApiRoutes(
@Named("required") private val authLens: RequiredAuthLens,
) : Supplier<RoutingHttpHandler> {
override fun get(): RoutingHttpHandler {
infix fun PathMethod.to(action: ProtectedHandler) =
this to { req: Request -> action(req, authLens(req)) }
@@ -38,9 +37,9 @@ class ApiRoutes(
"/search" bind POST to ::search,
"/{uuid}" bind GET to ::note,
"/{uuid}" bind PUT to ::update,
)
),
).withBasePath("/notes")
}
},
).withBasePath("/api")
}
+6 -11
View File
@@ -1,7 +1,6 @@
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
@@ -14,12 +13,11 @@ 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
import jakarta.inject.Named
import jakarta.inject.Singleton
@Singleton
class BasicRoutes(
private val healthCheckController: HealthCheckController,
private val baseCtrl: BaseController,
private val userCtrl: UserController,
private val noteCtrl: NoteController,
@@ -28,7 +26,6 @@ class BasicRoutes(
) : Supplier<RoutingHttpHandler> {
override fun get(): RoutingHttpHandler {
infix fun PathMethod.to(action: PublicHandler) =
this to { req: Request -> action(req, authLens(req)) }
@@ -36,8 +33,8 @@ class BasicRoutes(
static(
ResourceLoader.Classpath("/static"),
"woff2" to ContentType("font/woff2"),
"webmanifest" to ContentType("application/manifest+json")
)
"webmanifest" to ContentType("application/manifest+json"),
),
)
return routes(
@@ -50,11 +47,9 @@ class BasicRoutes(
"/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
),
staticHandler,
)
}
}
+3 -4
View File
@@ -12,8 +12,8 @@ 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
import jakarta.inject.Named
import jakarta.inject.Singleton
@Singleton
class NoteRoutes(
@@ -22,7 +22,6 @@ class NoteRoutes(
@Named("required") private val authLens: RequiredAuthLens,
) : Supplier<RoutingHttpHandler> {
override fun get(): RoutingHttpHandler {
infix fun PathMethod.to(action: ProtectedHandler) =
this to { req: Request -> action(req, authLens(req)) }
@@ -40,7 +39,7 @@ class NoteRoutes(
"/{uuid}/edit" bind POST to ::edit,
"/deleted/{uuid}" bind POST to ::deleted,
).withBasePath("/notes")
}
},
)
}
}
+2 -3
View File
@@ -9,7 +9,7 @@ import org.http4k.filter.ServerFilters.InitialiseRequestContext
import org.http4k.routing.RoutingHttpHandler
import org.http4k.routing.routes
import java.util.function.Supplier
import javax.inject.Singleton
import jakarta.inject.Singleton
@Singleton
class Router(
@@ -18,9 +18,8 @@ class Router(
private val subRouters: List<Supplier<RoutingHttpHandler>>,
) {
operator fun invoke(): RoutingHttpHandler {
val routes = routes(
*subRouters.map { it.get() }.toTypedArray()
*subRouters.map { it.get() }.toTypedArray(),
)
return errorFilter
+4 -4
View File
@@ -12,8 +12,8 @@ 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
import jakarta.inject.Named
import jakarta.inject.Singleton
@Singleton
class SettingsRoutes(
@@ -22,7 +22,6 @@ class SettingsRoutes(
@Named("required") private val authLens: RequiredAuthLens,
) : Supplier<RoutingHttpHandler> {
override fun get(): RoutingHttpHandler {
infix fun PathMethod.to(action: ProtectedHandler) =
this to { req: Request -> action(req, authLens(req)) }
@@ -31,7 +30,8 @@ class SettingsRoutes(
"/settings" bind GET to settingsController::settings,
"/settings" bind POST to settingsController::settings,
"/export" bind POST to settingsController::export,
)
"/import" bind POST to settingsController::import,
),
)
}
}
@@ -17,6 +17,6 @@ internal class LocalDateTimeSerializer : KSerializer<LocalDateTime> {
}
override fun deserialize(decoder: Decoder): LocalDateTime {
TODO("Not implemented, isn't needed")
return LocalDateTime.parse(decoder.decodeString())
}
}
+1 -1
View File
@@ -3,7 +3,7 @@ package be.simplenotes.app.utils
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.jsonObject
import kotlinx.serialization.json.jsonPrimitive
import javax.inject.Singleton
import jakarta.inject.Singleton
interface StaticFileResolver {
fun resolve(name: String): String?
+2 -2
View File
@@ -58,8 +58,8 @@ internal class RequiredAuthFilterTest {
},
"/protected" bind GET to requiredAuth.then { request: Request ->
Response(OK).body(requiredLens(request).toString())
}
)
},
),
)
// endregion
+5 -9
View File
@@ -2,18 +2,14 @@ plugins {
`kotlin-dsl`
}
kotlinDslPluginOptions {
experimentalWarning.set(false)
}
repositories {
gradlePluginPortal()
}
dependencies {
implementation("org.jetbrains.kotlin:kotlin-gradle-plugin:1.4.31")
implementation("org.jetbrains.kotlin:kotlin-serialization:1.4.31")
implementation("com.github.jengelman.gradle.plugins:shadow:6.1.0")
implementation("org.jlleitschuh.gradle:ktlint-gradle:9.4.1")
implementation("com.github.ben-manes:gradle-versions-plugin:0.28.0")
implementation("org.jetbrains.kotlin:kotlin-gradle-plugin:1.8.21")
implementation("org.jetbrains.kotlin:kotlin-serialization:1.8.21")
implementation("com.github.johnrengelman:shadow:8.1.1")
implementation("com.diffplug.spotless:spotless-plugin-gradle:6.13.0")
implementation("com.github.ben-manes:gradle-versions-plugin:0.46.0")
}
@@ -4,89 +4,10 @@ package be.simplenotes
object Libs {
object Flexmark {
private const val version = "0.62.2"
const val core = "com.vladsch.flexmark:flexmark:$version"
const val tasklist = "com.vladsch.flexmark:flexmark-ext-gfm-tasklist:$version"
}
object Database {
const val flyway = "org.flywaydb:flyway-core:7.5.4"
const val hikariCP = "com.zaxxer:HikariCP:4.0.2"
object Drivers {
const val h2 = "com.h2database:h2:1.4.200"
const val mariadb = "org.mariadb.jdbc:mariadb-java-client:2.7.2"
}
object Ktorm {
private const val version = "3.0.0"
const val core = "me.liuwj.ktorm:ktorm-core:$version"
const val mysql = "me.liuwj.ktorm:ktorm-support-mysql:$version"
}
}
object Lucene {
private const val version = "8.8.1"
const val core = "org.apache.lucene:lucene-core:$version"
const val analyzersCommon = "org.apache.lucene:lucene-analyzers-common:$version"
const val queryParser = "org.apache.lucene:lucene-queryparser:$version"
}
object Http4k {
private const val version = "4.3.5.4"
const val core = "org.http4k:http4k-core:$version"
const val testingHamkrest = "org.http4k:http4k-testing-hamkrest:$version"
}
object Jetty {
private const val version = "10.0.1"
const val server = "org.eclipse.jetty:jetty-server:$version"
const val servlet = "org.eclipse.jetty:jetty-servlet:$version"
}
object Kotlinx {
const val html = "org.jetbrains.kotlinx:kotlinx-html-jvm:0.7.2"
object Serialization {
const val json = "org.jetbrains.kotlinx:kotlinx-serialization-json-jvm:1.1.0"
}
}
object Slf4J {
const val api = "org.slf4j:slf4j-api:2.0.0-alpha1"
const val logback = "ch.qos.logback:logback-classic:1.3.0-alpha5"
}
object Mapstruct {
private const val version = "1.4.2.Final"
const val core = "org.mapstruct:mapstruct:$version"
const val processor = "org.mapstruct:mapstruct-processor:$version"
}
object Micronaut {
private const val version = "2.3.3"
private const val version = "4.0.0-M2"
const val inject = "io.micronaut:micronaut-inject:$version"
const val processor = "io.micronaut:micronaut-inject-java:$version"
}
const val arrowCoreData = "io.arrow-kt:arrow-core-data:0.11.0"
const val commonsCompress = "org.apache.commons:commons-compress:1.20"
const val javaJwt = "com.auth0:java-jwt:3.13.0"
const val javaxServlet = "javax.servlet:javax.servlet-api:4.0.1"
const val jbcrypt = "org.mindrot:jbcrypt:0.4"
const val konform = "io.konform:konform-jvm:0.2.0"
const val owaspHtmlSanitizer = "com.googlecode.owasp-java-html-sanitizer:owasp-java-html-sanitizer:20200713.1"
const val prettytime = "org.ocpsoft.prettytime:prettytime:5.0.0.Final"
const val snakeyaml = "org.yaml:snakeyaml:1.28"
object Test {
const val assertJ = "org.assertj:assertj-core:3.19.0"
const val hamkrest = "com.natpryce:hamkrest:1.8.0.1"
const val junit = "org.junit.jupiter:junit-jupiter:5.7.1"
const val mockk = "io.mockk:mockk:1.10.6"
const val faker = "com.github.javafaker:javafaker:1.0.2"
const val mariaTestContainer = "org.testcontainers:mariadb:1.15.2"
}
}
@@ -2,8 +2,10 @@ package be.simplenotes
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.api.plugins.JavaPluginConvention
import org.gradle.api.plugins.JavaPluginExtension
import org.gradle.api.tasks.SourceSetContainer
import org.gradle.kotlin.dsl.get
import org.gradle.kotlin.dsl.getByType
import org.gradle.kotlin.dsl.register
import java.io.File
@@ -17,7 +19,7 @@ class PostcssPlugin : Plugin<Project> {
getByName("processResources").dependsOn("postcss")
}
val sourceSets = project.convention.getPlugin(JavaPluginConvention::class.java).sourceSets
val sourceSets = project.extensions.getByType<SourceSetContainer>()
val root = File("${project.buildDir}/generated-resources/css")
sourceSets["main"].resources.srcDir(root)
}
@@ -11,6 +11,10 @@ tasks.withType<ShadowJar> {
archiveAppendix.set("with-dependencies")
manifest.attributes["Main-Class"] = "be.simplenotes.app.SimpleNotesKt"
// johnrengelman/shadow#449
// we need this for lucene-core
manifest.attributes["Multi-Release"] = "true"
mergeServiceFiles()
File(rootProject.projectDir, "buildSrc/src/main/resources/exclusions")
@@ -4,5 +4,11 @@ plugins {
id("be.simplenotes.java-convention")
id("be.simplenotes.kotlin-convention")
id("be.simplenotes.junit-convention")
id("org.jlleitschuh.gradle.ktlint")
id("com.diffplug.spotless")
}
spotless {
kotlin {
ktlint("0.48.0").setEditorConfigPath(project.rootProject.file(".editorconfig"))
}
}
@@ -6,24 +6,23 @@ plugins {
repositories {
mavenCentral()
maven {
url = uri("https://kotlin.bintray.com/kotlinx")
// https://github.com/Kotlin/kotlinx.html/issues/173
content { includeModule("org.jetbrains.kotlinx", "kotlinx-html-jvm") }
}
}
group = "be.simplenotes"
version = "1.0-SNAPSHOT"
java {
sourceCompatibility = JavaVersion.VERSION_15
targetCompatibility = JavaVersion.VERSION_15
toolchain {
languageVersion.set(JavaLanguageVersion.of(19))
vendor.set(JvmVendorSpec.ORACLE)
}
}
tasks.withType<JavaCompile> {
options.encoding = "UTF-8"
}
sourceSets["main"].resources.srcDirs("resources")
sourceSets["test"].resources.srcDirs("testresources")
sourceSets["main"].resources.setSrcDirs(listOf("resources"))
sourceSets["main"].java.setSrcDirs(emptyList<String>())
sourceSets["test"].resources.setSrcDirs(listOf("testresources"))
sourceSets["test"].java.setSrcDirs(emptyList<String>())
@@ -5,7 +5,9 @@ plugins {
}
tasks.withType<Test> {
useJUnitPlatform()
useJUnitPlatform {
excludeTags("slow")
}
}
dependencies {
@@ -1,5 +1,7 @@
package be.simplenotes
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
import org.jetbrains.kotlin.gradle.dsl.KotlinVersion
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
plugins {
@@ -13,17 +15,17 @@ dependencies {
}
tasks.withType<KotlinCompile> {
kotlinOptions {
jvmTarget = "15"
javaParameters = true
freeCompilerArgs = listOf(
compilerOptions {
jvmTarget.set(JvmTarget.JVM_19)
javaParameters.set(true)
freeCompilerArgs.addAll(
"-Xinline-classes",
"-Xno-param-assertions",
"-Xno-call-assertions",
"-Xno-receiver-assertions"
"-Xno-receiver-assertions",
)
}
}
kotlin.sourceSets["main"].kotlin.srcDirs("src")
kotlin.sourceSets["test"].kotlin.srcDirs("test")
kotlin.sourceSets["main"].kotlin.setSrcDirs(listOf("src"))
kotlin.sourceSets["test"].kotlin.setSrcDirs(listOf("test"))
@@ -10,12 +10,7 @@ tasks.named<DependencyUpdatesTask>("dependencyUpdates").configure {
resolutionStrategy {
componentSelection {
all {
if ("RC" in candidate.version) reject("Release candidate")
when {
candidate.group == "org.eclipse.jetty" && candidate.version.startsWith("11.") -> reject("javax -> jakarta")
candidate.group == "me.liuwj.ktorm" && candidate.version != "3.0.0" -> reject("SQL Case sensitivity changed")
}
if ("alpha|beta|rc".toRegex().containsMatchIn(candidate.version.lowercase())) reject("Non stable version")
}
}
}
@@ -1,5 +1,6 @@
META-INF/maven/**
META-INF/proguard/**
META-INF/com.android.tools/**
META-INF/*.kotlin_module
META-INF/DEPENDENCIES*
META-INF/NOTICE*
@@ -7,6 +8,7 @@ META-INF/LICENSE*
LICENSE*
META-INF/README*
META-INF/native-image/**
**/module-info.**
# Jetty
about.html
+3 -4
View File
@@ -1,4 +1,3 @@
import be.simplenotes.Libs
import be.simplenotes.micronaut
plugins {
@@ -8,8 +7,8 @@ plugins {
dependencies {
micronaut()
runtimeOnly(libs.yaml)
testImplementation(Libs.Test.junit)
testImplementation(Libs.Test.assertJ)
testRuntimeOnly(Libs.Slf4J.logback)
testImplementation(libs.bundles.test)
testRuntimeOnly(libs.slf4j.logback)
}
+2 -8
View File
@@ -1,11 +1,3 @@
db:
jdbc-url: jdbc:h2:./notes-db;
driver-class-name: org.h2.Driver
username: h2
password: ''
connection-timeout: 3000
maximum-pool-size: 10
jwt:
secret: 'PliLvfk7l4WF+cZJk66LR5Mpnh+ocbvJ2wfUCK2UCms='
validity: 24
@@ -14,3 +6,5 @@ jwt:
server:
host: localhost
port: 8080
data-dir: ./data
-33
View File
@@ -1,33 +0,0 @@
package be.simplenotes.config
import io.micronaut.context.annotation.ConfigurationInject
import io.micronaut.context.annotation.ConfigurationProperties
import java.util.concurrent.TimeUnit
@ConfigurationProperties("db")
data class DataSourceConfig @ConfigurationInject constructor(
val jdbcUrl: String,
val driverClassName: String,
val username: String,
val password: String,
val maximumPoolSize: Int,
val connectionTimeout: Long,
) {
override fun toString() = "DataSourceConfig(jdbcUrl='$jdbcUrl', driverClassName='$driverClassName', " +
"username='$username', password='***', maximumPoolSize=$maximumPoolSize, connectionTimeout=$connectionTimeout)"
}
@ConfigurationProperties("jwt")
data class JwtConfig @ConfigurationInject constructor(
val secret: String,
val validity: Long,
val timeUnit: TimeUnit,
) {
override fun toString() = "JwtConfig(secret='***', validity=$validity, timeUnit=$timeUnit)"
}
@ConfigurationProperties("server")
data class ServerConfig @ConfigurationInject constructor(
val host: String,
val port: Int,
)
+57
View File
@@ -0,0 +1,57 @@
package be.simplenotes.config
import io.micronaut.context.annotation.*
import java.nio.file.Path
import java.util.concurrent.TimeUnit
import jakarta.inject.Singleton
data class DataConfig(val dataDir: String)
data class DataSourceConfig(
val jdbcUrl: String,
val maximumPoolSize: Int,
val connectionTimeout: Long,
)
@ConfigurationProperties("jwt")
data class JwtConfig @ConfigurationInject constructor(
val secret: String,
val validity: Long,
val timeUnit: TimeUnit,
) {
override fun toString() = "JwtConfig(secret='***', validity=$validity, timeUnit=$timeUnit)"
}
@ConfigurationProperties("server")
data class ServerConfig @ConfigurationInject constructor(
val host: String,
val port: Int,
)
@Factory
class ConfigFactory {
@Singleton
@Requires(notEnv = ["test"])
fun datasourceConfig(dataConfig: DataConfig) = DataSourceConfig(
jdbcUrl = "jdbc:h2:" + Path.of(dataConfig.dataDir, "simplenotes").normalize().toAbsolutePath(),
maximumPoolSize = 10,
connectionTimeout = 1000,
)
@Singleton
@Requires(env = ["test"])
fun testDatasourceConfig() = DataSourceConfig(
jdbcUrl = "jdbc:h2:mem:regular;DB_CLOSE_DELAY=-1",
maximumPoolSize = 2,
connectionTimeout = 1000,
)
@Singleton
@Requires(notEnv = ["test"])
fun dataConfig(@Property(name = "data-dir") dataDir: String) = DataConfig(dataDir)
@Singleton
@Requires(env = ["test"])
fun testDataConfig() = DataConfig("/tmp")
}
+6 -7
View File
@@ -5,12 +5,11 @@
"//": "`gradle css`"
},
"dependencies": {
"autoprefixer": "^9.8.6",
"cssnano": "^4.1.10",
"postcss-cli": "^7.1.1",
"postcss-hash": "^2.0.0",
"postcss-import": "^12.0.1",
"postcss-nested": "^4.2.3",
"tailwindcss": "^1.5.1"
"autoprefixer": "^10.4.14",
"cssnano": "^6.0.1",
"postcss-cli": "^10.1.0",
"postcss-hash": "^3.0.0",
"postcss-import": "^15.1.0",
"tailwindcss": "^3.3.2"
}
}
+1 -1
View File
@@ -1,7 +1,7 @@
module.exports = {
plugins: [
require('postcss-import'),
require('postcss-nested'),
require('tailwindcss/nesting'),
require('tailwindcss'),
require('autoprefixer'),
require('cssnano')({
+1 -1
View File
@@ -2,7 +2,7 @@
@apply font-semibold py-2 px-4 rounded;
&:focus {
@apply outline-none shadow-outline;
@apply outline-none ring;
}
&:hover {
+1 -1
View File
@@ -1,6 +1,6 @@
#note {
a {
@apply text-blue-700 underline;
@apply text-blue-500 underline;
}
p {
+1 -1
View File
@@ -2,7 +2,7 @@
@apply italic font-semibold text-sm bg-teal-500 text-gray-900 rounded-full py-1 px-2 align-middle;
&:focus {
@apply outline-none shadow-outline bg-teal-800 text-white;
@apply outline-none ring bg-teal-800 text-white;
}
&:hover {
+1 -3
View File
@@ -1,9 +1,7 @@
module.exports = {
purge: {
content: [
process.env.PURGE
]
},
],
theme: {
fontFamily: {
'sans': [
+822 -1433
View File
File diff suppressed because it is too large Load Diff
+4 -41
View File
@@ -1,56 +1,19 @@
version: '2.2'
services:
db:
image: mariadb:10.5.5
container_name: simplenotes-mariadb
env_file:
- .env
environment:
- PUID=1000
- PGID=1000
- TZ=Europe/Brussels
- MYSQL_DATABASE=simplenotes
- MYSQL_USER=simplenotes
# .env:
# - MYSQL_ROOT_PASSWORD
# - MYSQL_PASSWORD
volumes:
- notes-db-volume:/var/lib/mysql
healthcheck:
test: "mysql --protocol=tcp -u simplenotes -p$MYSQL_PASSWORD -e 'show databases'"
interval: 5s
timeout: 1s
start_period: 2s
retries: 10
simplenotes:
image: hubv/simplenotes
container_name: simplenotes
env_file:
- .env
environment:
- PUID=1000
- PGID=1000
- TZ=Europe/Brussels
- SERVER_HOST=0.0.0.0
- DB_JDBC_URL=jdbc:mariadb://db:3306/simplenotes
- DB_DRIVER_CLASS_NAME=org.mariadb.jdbc.Driver
- DB_USERNAME=simplenotes
# .env:
# - JWT_SECRET
# - DB_PASSWORD
ports:
- 127.0.0.1:8080:8080
healthcheck:
test: "curl --fail -s http://localhost:8080/health"
interval: 5s
timeout: 1s
start_period: 2s
retries: 3
depends_on:
db:
condition: service_healthy
volumes:
notes-db-volume:
- ./simplenotes-data:/app/data
+10 -14
View File
@@ -1,4 +1,3 @@
import be.simplenotes.Libs
import be.simplenotes.micronaut
plugins {
@@ -13,21 +12,18 @@ dependencies {
implementation(project(":persistence"))
implementation(project(":search"))
api(Libs.arrowCoreData)
api(Libs.konform)
api(libs.arrow.core)
api(libs.konform)
micronaut()
implementation(Libs.Kotlinx.Serialization.json)
implementation(Libs.jbcrypt)
implementation(Libs.javaJwt)
implementation(Libs.Flexmark.core)
implementation(Libs.Flexmark.tasklist)
implementation(Libs.snakeyaml)
implementation(Libs.owaspHtmlSanitizer)
implementation(Libs.commonsCompress)
implementation(libs.kotlinx.serialization.json)
implementation(libs.bcrypt)
implementation(libs.jwt)
implementation(libs.bundles.flexmark)
implementation(libs.yaml)
implementation(libs.owasp.html.sanitizer)
implementation(libs.commonsCompress)
testImplementation(Libs.Test.hamkrest)
testImplementation(Libs.Test.junit)
testImplementation(Libs.Test.mockk)
testImplementation(libs.bundles.test)
}
@@ -1,8 +1,7 @@
package be.simplenotes.domain.usecases.export
package be.simplenotes.domain
import be.simplenotes.persistence.repositories.NoteRepository
import be.simplenotes.types.ExportedNote
import io.micronaut.context.annotation.Primary
import kotlinx.serialization.builtins.ListSerializer
import kotlinx.serialization.json.Json
import org.apache.commons.compress.archivers.zip.ZipArchiveEntry
@@ -10,14 +9,19 @@ import org.apache.commons.compress.archivers.zip.ZipArchiveOutputStream
import java.io.ByteArrayInputStream
import java.io.ByteArrayOutputStream
import java.io.InputStream
import javax.inject.Singleton
import jakarta.inject.Singleton
interface ExportService {
fun exportAsJson(userId: Int): String
fun exportAsZip(userId: Int): InputStream
}
@Primary
@Singleton
internal class ExportUseCaseImpl(
internal class ExportServiceImpl(
private val noteRepository: NoteRepository,
private val json: Json,
) : ExportUseCase {
) : ExportService {
override fun exportAsJson(userId: Int): String {
val notes = noteRepository.export(userId)
return json.encodeToString(ListSerializer(ExportedNote.serializer()), notes)
@@ -38,7 +42,7 @@ internal class ExportUseCaseImpl(
}
}
class ZipOutput : AutoCloseable {
private class ZipOutput : AutoCloseable {
val outputStream = ByteArrayOutputStream()
private val zipOutputStream = ZipArchiveOutputStream(outputStream)
+30
View File
@@ -0,0 +1,30 @@
package be.simplenotes.domain
import be.simplenotes.domain.security.HtmlSanitizer
import be.simplenotes.persistence.repositories.NoteRepository
import be.simplenotes.types.ExportedNote
import be.simplenotes.types.LoggedInUser
import jakarta.inject.Singleton
import kotlinx.serialization.builtins.ListSerializer
import kotlinx.serialization.json.Json
interface ImportService {
fun importJson(user: LoggedInUser, content: String)
}
@Singleton
internal class ImportServiceImpl(
private val noteRepository: NoteRepository,
private val json: Json,
private val htmlSanitizer: HtmlSanitizer,
) : ImportService {
override fun importJson(user: LoggedInUser, content: String) {
val notes = json.decodeFromString(ListSerializer(ExportedNote.serializer()), content)
noteRepository.import(
user.userId,
notes.map {
it.copy(html = htmlSanitizer.sanitize(user, it.html))
},
)
}
}
-1
View File
@@ -1 +0,0 @@
package be.simplenotes.domain
+73
View File
@@ -0,0 +1,73 @@
package be.simplenotes.domain
import arrow.core.Either
import arrow.core.raise.either
import arrow.core.raise.ensure
import be.simplenotes.domain.validation.NoteValidations
import be.simplenotes.types.NoteMetadata
import com.vladsch.flexmark.html.HtmlRenderer
import com.vladsch.flexmark.parser.Parser
import io.konform.validation.ValidationErrors
import jakarta.inject.Singleton
import org.yaml.snakeyaml.Yaml
import org.yaml.snakeyaml.parser.ParserException
import org.yaml.snakeyaml.scanner.ScannerException
interface MarkdownService {
fun renderDocument(input: String): Either<MarkdownParsingError, Document>
}
@Singleton
internal class MarkdownServiceImpl(
private val parser: Parser,
private val renderer: HtmlRenderer,
) : MarkdownService {
private val yamlBoundPattern = "-{3}".toRegex()
private fun splitMetaFromDocument(input: String) = either {
val split = input.split(yamlBoundPattern, 3)
ensure(split.size >= 3) { MarkdownParsingError.MissingMeta }
split[1].trim() to split[2].trim()
}
private val yaml = Yaml()
private fun parseMeta(input: String) = either {
val load: Map<String, Any> = try {
yaml.load(input)
} catch (e: ParserException) {
raise(MarkdownParsingError.InvalidMeta)
} catch (e: ScannerException) {
raise(MarkdownParsingError.InvalidMeta)
}
val title = when (val titleNode = load["title"]) {
is String, is Number -> titleNode.toString()
else -> raise(MarkdownParsingError.InvalidMeta)
}
val tagsNode = load["tags"]
val tags = if (tagsNode !is List<*>)
emptyList()
else
tagsNode.map { it.toString() }
NoteMetadata(title, tags)
}
private fun renderMarkdown(markdown: String) = parser.parse(markdown).run(renderer::render)
override fun renderDocument(input: String) = either {
val (meta, md) = splitMetaFromDocument(input).bind()
val parsedMeta = parseMeta(meta).bind()
NoteValidations.validateMetadata(parsedMeta)?.let { raise(it) }
val html = renderMarkdown(md)
Document(parsedMeta, html)
}
}
sealed class MarkdownParsingError {
object MissingMeta : MarkdownParsingError()
object InvalidMeta : MarkdownParsingError()
class ValidationError(val validationErrors: ValidationErrors) : MarkdownParsingError()
}
data class Document(val metadata: NoteMetadata, val html: String)
@@ -1,59 +1,52 @@
package be.simplenotes.domain.usecases
package be.simplenotes.domain
import arrow.core.computations.either
import arrow.core.raise.either
import be.simplenotes.domain.security.HtmlSanitizer
import be.simplenotes.domain.usecases.markdown.MarkdownConverter
import be.simplenotes.domain.usecases.markdown.MarkdownParsingError
import be.simplenotes.domain.usecases.search.parseSearchTerms
import be.simplenotes.domain.utils.parseSearchTerms
import be.simplenotes.persistence.repositories.NoteRepository
import be.simplenotes.persistence.repositories.UserRepository
import be.simplenotes.persistence.transactions.TransactionService
import be.simplenotes.search.NoteSearcher
import be.simplenotes.types.LoggedInUser
import be.simplenotes.types.Note
import be.simplenotes.types.PersistedNote
import be.simplenotes.types.PersistedNoteMetadata
import jakarta.annotation.PostConstruct
import jakarta.annotation.PreDestroy
import jakarta.inject.Singleton
import java.util.*
import javax.annotation.PostConstruct
import javax.annotation.PreDestroy
import javax.inject.Singleton
@Singleton
class NoteService(
private val markdownConverter: MarkdownConverter,
private val markdownService: MarkdownService,
private val noteRepository: NoteRepository,
private val userRepository: UserRepository,
private val searcher: NoteSearcher,
private val htmlSanitizer: HtmlSanitizer,
private val transaction: TransactionService,
) {
fun create(user: LoggedInUser, markdownText: String) = transaction.use {
either.eager<MarkdownParsingError, PersistedNote> {
val persistedNote = !markdownConverter.renderDocument(markdownText)
fun create(user: LoggedInUser, markdownText: String) = either {
markdownService.renderDocument(markdownText)
.map { it.copy(html = htmlSanitizer.sanitize(user, it.html)) }
.map { Note(it.metadata, markdown = markdownText, html = it.html) }
.map { Note(title = it.metadata.title, tags = it.metadata.tags, markdown = markdownText, html = it.html) }
.map { noteRepository.create(user.userId, it) }
searcher.indexNote(user.userId, persistedNote)
persistedNote
}
.bind()
.also { searcher.indexNote(user.userId, it) }
}
fun update(
user: LoggedInUser,
uuid: UUID,
markdownText: String,
) = transaction.use {
either.eager<MarkdownParsingError, PersistedNote?> {
val persistedNote = !markdownConverter.renderDocument(markdownText)
fun update(user: LoggedInUser, uuid: UUID, markdownText: String) =
either {
markdownService.renderDocument(markdownText)
.map { it.copy(html = htmlSanitizer.sanitize(user, it.html)) }
.map { Note(it.metadata, markdown = markdownText, html = it.html) }
.map { noteRepository.update(user.userId, uuid, it) }
persistedNote?.let { searcher.updateIndex(user.userId, it) }
persistedNote
.map {
Note(
title = it.metadata.title,
tags = it.metadata.tags,
markdown = markdownText,
html = it.html,
)
}
.map { noteRepository.update(user.userId, uuid, it) }
.bind()
?.also { searcher.updateIndex(user.userId, it) }
}
fun paginatedNotes(
@@ -72,22 +65,22 @@ class NoteService(
fun find(userId: Int, uuid: UUID) = noteRepository.find(userId, uuid)
fun trash(userId: Int, uuid: UUID): Boolean = transaction.use {
fun trash(userId: Int, uuid: UUID): Boolean {
val res = noteRepository.delete(userId, uuid, permanent = false)
if (res) searcher.deleteIndex(userId, uuid)
res
return res
}
fun restore(userId: Int, uuid: UUID): Boolean = transaction.use {
fun restore(userId: Int, uuid: UUID): Boolean {
val res = noteRepository.restore(userId, uuid)
if (res) find(userId, uuid)?.let { note -> searcher.indexNote(userId, note) }
res
return res
}
fun delete(userId: Int, uuid: UUID): Boolean = transaction.use {
fun delete(userId: Int, uuid: UUID): Boolean {
val res = noteRepository.delete(userId, uuid, permanent = true)
if (res) searcher.deleteIndex(userId, uuid)
res
return res
}
fun countDeleted(userId: Int) = noteRepository.count(userId, deleted = true)
@@ -107,8 +100,8 @@ class NoteService(
@PreDestroy
fun dropAllIndexes() = searcher.dropAll()
fun makePublic(userId: Int, uuid: UUID) = transaction.use { noteRepository.makePublic(userId, uuid) }
fun makePrivate(userId: Int, uuid: UUID) = transaction.use { noteRepository.makePrivate(userId, uuid) }
fun makePublic(userId: Int, uuid: UUID) = noteRepository.makePublic(userId, uuid)
fun makePrivate(userId: Int, uuid: UUID) = noteRepository.makePrivate(userId, uuid)
fun findPublic(uuid: UUID) = noteRepository.findPublic(uuid)
}
+80
View File
@@ -0,0 +1,80 @@
package be.simplenotes.domain
import arrow.core.Either
import arrow.core.raise.either
import arrow.core.raise.ensure
import arrow.core.raise.ensureNotNull
import be.simplenotes.domain.security.PasswordHash
import be.simplenotes.domain.security.SimpleJwt
import be.simplenotes.domain.validation.UserValidations
import be.simplenotes.persistence.repositories.UserRepository
import be.simplenotes.search.NoteSearcher
import be.simplenotes.types.LoggedInUser
import be.simplenotes.types.PersistedUser
import io.konform.validation.ValidationErrors
import jakarta.inject.Singleton
import kotlinx.serialization.Serializable
interface UserService {
fun register(form: RegisterForm): Either<RegisterError, PersistedUser>
fun login(form: LoginForm): Either<LoginError, Token>
fun delete(form: DeleteForm): Either<DeleteError, Unit>
}
@Singleton
internal class UserServiceImpl(
private val userRepository: UserRepository,
private val passwordHash: PasswordHash,
private val jwt: SimpleJwt<LoggedInUser>,
private val searcher: NoteSearcher,
) : UserService {
override fun register(form: RegisterForm) = either {
val user = UserValidations.validateRegister(form).bind()
ensure(!userRepository.exists(user.username)) { RegisterError.UserExists }
ensureNotNull(userRepository.create(user.copy(password = passwordHash.crypt(user.password)))) {
RegisterError.UserExists
}
}
override fun login(form: LoginForm) = either {
val user = UserValidations.validateLogin(form).bind()
val persistedUser = ensureNotNull(userRepository.find(user.username)) { LoginError.Unregistered }
ensure(passwordHash.verify(form.password!!, persistedUser.password)) { LoginError.WrongPassword }
jwt.sign(LoggedInUser(persistedUser))
}
override fun delete(form: DeleteForm) = either {
val user = UserValidations.validateDelete(form).bind()
val persistedUser = ensureNotNull(userRepository.find(user.username)) { DeleteError.Unregistered }
ensure(passwordHash.verify(user.password, persistedUser.password)) { DeleteError.WrongPassword }
ensure(userRepository.delete(persistedUser.id)) { DeleteError.Unregistered }
searcher.dropIndex(persistedUser.id)
}
}
sealed class DeleteError {
object Unregistered : DeleteError()
object WrongPassword : DeleteError()
class InvalidForm(val validationErrors: ValidationErrors) : DeleteError()
}
data class DeleteForm(val username: String?, val password: String?, val checked: Boolean)
sealed class LoginError {
object Unregistered : LoginError()
object WrongPassword : LoginError()
class InvalidLoginForm(val validationErrors: ValidationErrors) : LoginError()
}
typealias Token = String
sealed class RegisterError {
object UserExists : RegisterError()
class InvalidRegisterForm(val validationErrors: ValidationErrors) : RegisterError()
}
typealias RegisterForm = LoginForm
@Serializable
data class LoginForm(val username: String?, val password: String?)
@@ -1,11 +1,11 @@
package be.simplenotes.domain.usecases.markdown
package be.simplenotes.domain.modules
import com.vladsch.flexmark.ext.gfm.tasklist.TaskListExtension
import com.vladsch.flexmark.html.HtmlRenderer
import com.vladsch.flexmark.parser.Parser
import com.vladsch.flexmark.util.data.MutableDataSet
import io.micronaut.context.annotation.Factory
import javax.inject.Singleton
import jakarta.inject.Singleton
@Factory
class FlexmarkFactory {
@@ -23,11 +23,11 @@ class FlexmarkFactory {
set(TaskListExtension.LOOSE_ITEM_CLASS, "")
set(
TaskListExtension.ITEM_DONE_MARKER,
"""<input type="checkbox" checked="checked" disabled="disabled" readonly="readonly" />&nbsp;"""
"""<input type="checkbox" checked="checked" disabled="disabled" readonly="readonly" />&nbsp;""",
)
set(
TaskListExtension.ITEM_NOT_DONE_MARKER,
"""<input type="checkbox" disabled="disabled" readonly="readonly" />&nbsp;"""
"""<input type="checkbox" disabled="disabled" readonly="readonly" />&nbsp;""",
)
set(HtmlRenderer.SOFT_BREAK, "<br>")
}
+1 -1
View File
@@ -4,7 +4,7 @@ import be.simplenotes.types.LoggedInUser
import org.owasp.html.HtmlChangeListener
import org.owasp.html.HtmlPolicyBuilder
import org.slf4j.LoggerFactory
import javax.inject.Singleton
import jakarta.inject.Singleton
@Singleton
class HtmlSanitizer {
+1 -1
View File
@@ -3,7 +3,7 @@ package be.simplenotes.domain.security
import be.simplenotes.types.LoggedInUser
import com.auth0.jwt.JWTCreator
import com.auth0.jwt.interfaces.DecodedJWT
import javax.inject.Singleton
import jakarta.inject.Singleton
interface JwtMapper<T> {
fun extract(decodedJWT: DecodedJWT): T?
+2 -2
View File
@@ -1,8 +1,8 @@
package be.simplenotes.domain.security
import org.mindrot.jbcrypt.BCrypt
import javax.inject.Inject
import javax.inject.Singleton
import jakarta.inject.Inject
import jakarta.inject.Singleton
internal interface PasswordHash {
fun crypt(password: String): String
+1 -1
View File
@@ -7,7 +7,7 @@ import com.auth0.jwt.algorithms.Algorithm
import com.auth0.jwt.exceptions.JWTVerificationException
import java.util.*
import java.util.concurrent.TimeUnit
import javax.inject.Singleton
import jakarta.inject.Singleton
@Singleton
class SimpleJwt<T>(jwtConfig: JwtConfig, private val mapper: JwtMapper<T>) {
-13
View File
@@ -1,13 +0,0 @@
package be.simplenotes.domain.usecases
import be.simplenotes.persistence.DbHealthCheck
import javax.inject.Singleton
interface HealthCheckService {
fun isOk(): Boolean
}
@Singleton
class HealthCheckServiceImpl(private val dbHealthCheck: DbHealthCheck) : HealthCheckService {
override fun isOk() = dbHealthCheck.isOk()
}
-18
View File
@@ -1,18 +0,0 @@
package be.simplenotes.domain.usecases
import be.simplenotes.domain.usecases.export.ExportUseCase
import be.simplenotes.domain.usecases.users.delete.DeleteUseCase
import be.simplenotes.domain.usecases.users.login.LoginUseCase
import be.simplenotes.domain.usecases.users.register.RegisterUseCase
import javax.inject.Singleton
@Singleton
class UserService(
loginUseCase: LoginUseCase,
registerUseCase: RegisterUseCase,
deleteUseCase: DeleteUseCase,
exportUseCase: ExportUseCase,
) : LoginUseCase by loginUseCase,
RegisterUseCase by registerUseCase,
DeleteUseCase by deleteUseCase,
ExportUseCase by exportUseCase
@@ -1,8 +0,0 @@
package be.simplenotes.domain.usecases.export
import java.io.InputStream
interface ExportUseCase {
fun exportAsJson(userId: Int): String
fun exportAsZip(userId: Int): InputStream
}
@@ -1,16 +0,0 @@
package be.simplenotes.domain.usecases.markdown
import arrow.core.Either
import be.simplenotes.types.NoteMetadata
import io.konform.validation.ValidationErrors
sealed class MarkdownParsingError
object MissingMeta : MarkdownParsingError()
object InvalidMeta : MarkdownParsingError()
class ValidationError(val validationErrors: ValidationErrors) : MarkdownParsingError()
data class Document(val metadata: NoteMetadata, val html: String)
interface MarkdownConverter {
fun renderDocument(input: String): Either<MarkdownParsingError, Document>
}
@@ -1,63 +0,0 @@
package be.simplenotes.domain.usecases.markdown
import arrow.core.Either
import arrow.core.computations.either
import arrow.core.left
import arrow.core.right
import be.simplenotes.domain.validation.NoteValidations
import be.simplenotes.types.NoteMetadata
import com.vladsch.flexmark.html.HtmlRenderer
import com.vladsch.flexmark.parser.Parser
import org.yaml.snakeyaml.Yaml
import org.yaml.snakeyaml.parser.ParserException
import org.yaml.snakeyaml.scanner.ScannerException
import javax.inject.Singleton
private typealias MetaMdPair = Pair<String, String>
@Singleton
internal class MarkdownConverterImpl(
private val parser: Parser,
private val renderer: HtmlRenderer,
) : MarkdownConverter {
private val yamlBoundPattern = "-{3}".toRegex()
private fun splitMetaFromDocument(input: String): Either<MissingMeta, MetaMdPair> {
val split = input.split(yamlBoundPattern, 3)
if (split.size < 3) return MissingMeta.left()
return (split[1].trim() to split[2].trim()).right()
}
private val yaml = Yaml()
private fun parseMeta(input: String): Either<InvalidMeta, NoteMetadata> {
val load: Map<String, Any> = try {
yaml.load(input)
} catch (e: ParserException) {
return InvalidMeta.left()
} catch (e: ScannerException) {
return InvalidMeta.left()
}
val title = when (val titleNode = load["title"]) {
is String, is Number -> titleNode.toString()
else -> return InvalidMeta.left()
}
val tagsNode = load["tags"]
val tags = if (tagsNode !is List<*>)
emptyList()
else
tagsNode.map { it.toString() }
return NoteMetadata(title, tags).right()
}
private fun renderMarkdown(markdown: String) = parser.parse(markdown).run(renderer::render)
override fun renderDocument(input: String) = either.eager<MarkdownParsingError, Document> {
val (meta, md) = !splitMetaFromDocument(input)
val parsedMeta = !parseMeta(meta)
!Either.fromNullable(NoteValidations.validateMetadata(parsedMeta)).swap()
val html = renderMarkdown(md)
Document(parsedMeta, html)
}
}
@@ -1,43 +0,0 @@
package be.simplenotes.domain.usecases.search
import be.simplenotes.search.SearchTerms
private fun innerRegex(name: String) =
"""$name:['"](.*?)['"]""".toRegex()
private fun outerRegex(name: String) =
"""($name:['"].*?['"])""".toRegex()
private val titleRe = innerRegex("title")
private val outerTitleRe = outerRegex("title")
private val tagRe = innerRegex("tag")
private val outerTagRe = outerRegex("tag")
private val contentRe = innerRegex("content")
private val outerContentRe = outerRegex("content")
internal fun parseSearchTerms(input: String): SearchTerms {
var c: String = input
fun extract(innerRegex: Regex, outerRegex: Regex): String? {
val match = innerRegex.find(input)?.groups?.get(1)?.value
if (match != null) {
val group = outerRegex.find(input)?.groups?.get(1)?.value
group?.let { c = c.replace(it, "") }
}
return match
}
val title: String? = extract(titleRe, outerTitleRe)
val tag: String? = extract(tagRe, outerTagRe)
val content: String? = extract(contentRe, outerContentRe)
val all = c.trim().ifEmpty { null }
return SearchTerms(
title = title,
tag = tag,
content = content,
all = all
)
}
@@ -1,35 +0,0 @@
package be.simplenotes.domain.usecases.users.delete
import arrow.core.Either
import arrow.core.computations.either
import arrow.core.rightIfNotNull
import be.simplenotes.domain.security.PasswordHash
import be.simplenotes.domain.validation.UserValidations
import be.simplenotes.persistence.repositories.UserRepository
import be.simplenotes.persistence.transactions.TransactionService
import be.simplenotes.search.NoteSearcher
import io.micronaut.context.annotation.Primary
import javax.inject.Singleton
@Primary
@Singleton
internal class DeleteUseCaseImpl(
private val userRepository: UserRepository,
private val passwordHash: PasswordHash,
private val searcher: NoteSearcher,
private val transactionService: TransactionService,
) : DeleteUseCase {
override fun delete(form: DeleteForm) = transactionService.use {
either.eager<DeleteError, Unit> {
val user = !UserValidations.validateDelete(form)
val persistedUser = !userRepository.find(user.username).rightIfNotNull { DeleteError.Unregistered }
!Either.conditionally(
passwordHash.verify(user.password, persistedUser.password),
{ DeleteError.WrongPassword },
{ Unit }
)
!Either.conditionally(userRepository.delete(persistedUser.id), { DeleteError.Unregistered }, { Unit })
searcher.dropIndex(persistedUser.id)
}
}
}
@@ -1,16 +0,0 @@
package be.simplenotes.domain.usecases.users.delete
import arrow.core.Either
import io.konform.validation.ValidationErrors
sealed class DeleteError {
object Unregistered : DeleteError()
object WrongPassword : DeleteError()
class InvalidForm(val validationErrors: ValidationErrors) : DeleteError()
}
data class DeleteForm(val username: String?, val password: String?, val checked: Boolean)
interface DeleteUseCase {
fun delete(form: DeleteForm): Either<DeleteError, Unit>
}
@@ -1,28 +0,0 @@
package be.simplenotes.domain.usecases.users.login
import arrow.core.computations.either
import arrow.core.filterOrElse
import arrow.core.rightIfNotNull
import be.simplenotes.domain.security.PasswordHash
import be.simplenotes.domain.security.SimpleJwt
import be.simplenotes.domain.validation.UserValidations
import be.simplenotes.persistence.repositories.UserRepository
import be.simplenotes.types.LoggedInUser
import io.micronaut.context.annotation.Primary
import javax.inject.Singleton
@Singleton
@Primary
internal class LoginUseCaseImpl(
private val userRepository: UserRepository,
private val passwordHash: PasswordHash,
private val jwt: SimpleJwt<LoggedInUser>
) : LoginUseCase {
override fun login(form: LoginForm) = either.eager<LoginError, Token> {
val user = !UserValidations.validateLogin(form)
!userRepository.find(user.username)
.rightIfNotNull { Unregistered }
.filterOrElse({ passwordHash.verify(form.password!!, it.password) }, { WrongPassword })
.map { jwt.sign(LoggedInUser(it)) }
}
}
@@ -1,19 +0,0 @@
package be.simplenotes.domain.usecases.users.login
import arrow.core.Either
import io.konform.validation.ValidationErrors
import kotlinx.serialization.Serializable
sealed class LoginError
object Unregistered : LoginError()
object WrongPassword : LoginError()
class InvalidLoginForm(val validationErrors: ValidationErrors) : LoginError()
typealias Token = String
@Serializable
data class LoginForm(val username: String?, val password: String?)
interface LoginUseCase {
fun login(form: LoginForm): Either<LoginError, Token>
}
@@ -1,28 +0,0 @@
package be.simplenotes.domain.usecases.users.register
import arrow.core.Either
import arrow.core.filterOrElse
import arrow.core.leftIfNull
import be.simplenotes.domain.security.PasswordHash
import be.simplenotes.domain.validation.UserValidations
import be.simplenotes.persistence.repositories.UserRepository
import be.simplenotes.persistence.transactions.TransactionService
import be.simplenotes.types.PersistedUser
import io.micronaut.context.annotation.Primary
import javax.inject.Singleton
@Primary
@Singleton
internal class RegisterUseCaseImpl(
private val userRepository: UserRepository,
private val passwordHash: PasswordHash,
private val transactionService: TransactionService,
) : RegisterUseCase {
override fun register(form: RegisterForm): Either<RegisterError, PersistedUser> = transactionService.use {
UserValidations.validateRegister(form)
.filterOrElse({ !userRepository.exists(it.username) }, { UserExists })
.map { it.copy(password = passwordHash.crypt(it.password)) }
.map { userRepository.create(it) }
.leftIfNull { UserExists }
}
}
@@ -1,16 +0,0 @@
package be.simplenotes.domain.usecases.users.register
import arrow.core.Either
import be.simplenotes.domain.usecases.users.login.LoginForm
import be.simplenotes.types.PersistedUser
import io.konform.validation.ValidationErrors
sealed class RegisterError
object UserExists : RegisterError()
class InvalidRegisterForm(val validationErrors: ValidationErrors) : RegisterError()
typealias RegisterForm = LoginForm
interface RegisterUseCase {
fun register(form: RegisterForm): Either<RegisterError, PersistedUser>
}
+94
View File
@@ -0,0 +1,94 @@
package be.simplenotes.domain.utils
import be.simplenotes.search.SearchTerms
import java.util.*
private enum class Quote { SingleQuote, DoubleQuote, }
private data class ParsedSearchInput(val global: List<String>, val entries: Map<String, String>)
private fun parseInput(input: String): ParsedSearchInput {
val tokenizer = StringTokenizer(input, ":\"' ", true)
val tokens = ArrayList<String>()
val current = StringBuilder()
var quoteOpen: Quote? = null
fun push() {
if (current.isNotEmpty()) {
tokens.add(current.toString())
}
current.setLength(0)
quoteOpen = null
}
while (tokenizer.hasMoreTokens()) {
when (val token = tokenizer.nextToken()) {
"\"" -> when {
Quote.DoubleQuote == quoteOpen -> push()
quoteOpen == null -> quoteOpen = Quote.DoubleQuote
else -> current.append(token)
}
"'" -> when {
Quote.SingleQuote == quoteOpen -> push()
quoteOpen == null -> quoteOpen = Quote.SingleQuote
else -> current.append(token)
}
" " -> {
if (quoteOpen != null) current.append(" ")
else push()
}
":" -> {
push()
tokens.add(token)
}
else -> {
current.append(token)
}
}
}
push()
val entries = HashMap<String, String>()
val colonIndexes = ArrayList<Int>()
tokens.forEachIndexed { index, token ->
if (token == ":") colonIndexes += index
}
var changes = 0
for (colonIndex in colonIndexes) {
val offset = changes * 3
val key = tokens.getOrNull(colonIndex - 1 - offset)
val value = tokens.getOrNull(colonIndex + 1 - offset)
if (key != null && value != null) {
entries[key] = value
tokens.removeAt(colonIndex - 1 - offset) // remove key
tokens.removeAt(colonIndex - 1 - offset) // remove :
tokens.removeAt(colonIndex - 1 - offset) // remove value
changes++
}
}
return ParsedSearchInput(global = tokens, entries = entries)
}
internal fun parseSearchTerms(input: String): SearchTerms {
val parsedInput = parseInput(input)
val title: String? = parsedInput.entries["title"]
val tag: String? = parsedInput.entries["tag"]
val content: String? = parsedInput.entries["content"]
val all = parsedInput.global.takeIf { it.isNotEmpty() }?.joinToString(" ")
return SearchTerms(
title = title,
tag = tag,
content = content,
all = all,
)
}
+3 -3
View File
@@ -1,6 +1,6 @@
package be.simplenotes.domain.validation
import be.simplenotes.domain.usecases.markdown.ValidationError
import be.simplenotes.domain.MarkdownParsingError
import be.simplenotes.types.NoteMetadata
import io.konform.validation.Validation
import io.konform.validation.jsonschema.maxItems
@@ -27,9 +27,9 @@ internal object NoteValidations {
}
}
fun validateMetadata(meta: NoteMetadata): ValidationError? {
fun validateMetadata(meta: NoteMetadata): MarkdownParsingError.ValidationError? {
val errors = metaValidator.validate(meta).errors
return if (errors.isEmpty()) null
else return ValidationError(errors)
else return MarkdownParsingError.ValidationError(errors)
}
}
+12 -18
View File
@@ -1,14 +1,8 @@
package be.simplenotes.domain.validation
import arrow.core.Either
import arrow.core.left
import arrow.core.right
import be.simplenotes.domain.usecases.users.delete.DeleteError
import be.simplenotes.domain.usecases.users.delete.DeleteForm
import be.simplenotes.domain.usecases.users.login.InvalidLoginForm
import be.simplenotes.domain.usecases.users.login.LoginForm
import be.simplenotes.domain.usecases.users.register.InvalidRegisterForm
import be.simplenotes.domain.usecases.users.register.RegisterForm
import arrow.core.raise.either
import arrow.core.raise.ensure
import be.simplenotes.domain.*
import be.simplenotes.types.User
import io.konform.validation.Validation
import io.konform.validation.jsonschema.maxLength
@@ -26,16 +20,16 @@ internal object UserValidations {
}
}
fun validateLogin(form: LoginForm): Either<InvalidLoginForm, User> {
fun validateLogin(form: LoginForm) = either<LoginError.InvalidLoginForm, User> {
val errors = loginValidator.validate(form).errors
return if (errors.isEmpty()) User(form.username!!, form.password!!).right()
else return InvalidLoginForm(errors).left()
ensure(errors.isEmpty()) { LoginError.InvalidLoginForm(errors) }
User(form.username!!, form.password!!)
}
fun validateRegister(form: RegisterForm): Either<InvalidRegisterForm, User> {
fun validateRegister(form: RegisterForm) = either {
val errors = loginValidator.validate(form).errors
return if (errors.isEmpty()) User(form.username!!, form.password!!).right()
else return InvalidRegisterForm(errors).left()
ensure(errors.isEmpty()) { RegisterError.InvalidRegisterForm(errors) }
User(form.username!!, form.password!!)
}
private val deleteValidator = Validation<DeleteForm> {
@@ -52,9 +46,9 @@ internal object UserValidations {
}
}
fun validateDelete(form: DeleteForm): Either<DeleteError.InvalidForm, User> {
fun validateDelete(form: DeleteForm) = either {
val errors = deleteValidator.validate(form).errors
return if (errors.isEmpty()) User(form.username!!, form.password!!).right()
else return DeleteError.InvalidForm(errors).left()
ensure(errors.isEmpty()) { DeleteError.InvalidForm(errors) }
User(form.username!!, form.password!!)
}
}
-1
View File
@@ -1 +0,0 @@
package be.simplenotes.domain
+96
View File
@@ -0,0 +1,96 @@
@file:Suppress("MemberVisibilityCanBePrivate")
package be.simplenotes.domain
import be.simplenotes.config.JwtConfig
import be.simplenotes.domain.security.BcryptPasswordHash
import be.simplenotes.domain.security.SimpleJwt
import be.simplenotes.domain.security.UserJwtMapper
import be.simplenotes.domain.testutils.isLeftOfType
import be.simplenotes.domain.testutils.isRight
import be.simplenotes.persistence.repositories.UserRepository
import be.simplenotes.types.PersistedUser
import com.natpryce.hamkrest.assertion.assertThat
import com.natpryce.hamkrest.equalTo
import io.mockk.*
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import java.util.concurrent.TimeUnit
internal class UserServiceTest {
val userRepository = mockk<UserRepository>()
val passwordHash = BcryptPasswordHash(test = true)
val jwtConfig = JwtConfig("a secret", 1, TimeUnit.HOURS)
val simpleJwt = SimpleJwt(jwtConfig, UserJwtMapper())
val userService = UserServiceImpl(
userRepository = userRepository,
passwordHash = passwordHash,
jwt = simpleJwt,
searcher = mockk(),
)
@BeforeEach
fun resetMocks() {
clearMocks(userRepository)
}
@Test
fun `register should fail with invalid form`() {
val form = RegisterForm("", "a".repeat(10))
assertThat(userService.register(form), isLeftOfType<RegisterError.InvalidRegisterForm>())
verify { userRepository wasNot called }
}
@Test
fun `Register should fail with existing username`() {
val form = RegisterForm("someuser", "somepassword")
every { userRepository.exists(form.username!!) } returns true
assertThat(userService.register(form), isLeftOfType<RegisterError.UserExists>())
}
@Test
fun `Register should succeed with new user`() {
val form = RegisterForm("someuser", "somepassword")
every { userRepository.exists(form.username!!) } returns false
every { userRepository.create(any()) } returns PersistedUser(form.username!!, form.password!!, 1)
val res = userService.register(form)
assertThat(res, isRight())
res.map { assertThat(it.username, equalTo(form.username)) }
}
@Test
fun `Login should fail with invalid form`() {
val form = LoginForm("", "a")
assertThat(userService.login(form), isLeftOfType<LoginError.InvalidLoginForm>())
verify { userRepository wasNot called }
}
@Test
fun `Login should fail with non existing user`() {
val form = LoginForm("someusername", "somepassword")
every { userRepository.find(form.username!!) } returns null
assertThat(userService.login(form), isLeftOfType<LoginError.Unregistered>())
}
@Test
fun `Login should fail with wrong password`() {
val form = LoginForm("someusername", "wrongpassword")
every { userRepository.find(form.username!!) } returns
PersistedUser(form.username!!, passwordHash.crypt("right password"), 1)
assertThat(userService.login(form), isLeftOfType<LoginError.WrongPassword>())
}
@Test
fun `Login should succeed with existing user and correct password`() {
val loginForm = LoginForm("someusername", "somepassword")
every { userRepository.find(loginForm.username!!) } returns
PersistedUser(loginForm.username!!, passwordHash.crypt(loginForm.password!!), 1)
val res = userService.login(loginForm)
assertThat(res, isRight())
}
}
@@ -1,7 +1,7 @@
package be.simplenotes.domain.security
import be.simplenotes.config.JwtConfig
import be.simplenotes.domain.usecases.users.login.Token
import be.simplenotes.domain.Token
import be.simplenotes.types.LoggedInUser
import com.auth0.jwt.JWT
import com.auth0.jwt.algorithms.Algorithm
@@ -33,7 +33,7 @@ internal class LoggedInUserExtractorTest {
createToken(),
createToken(username = "user", id = 1, secret = "not the correct secret"),
createToken(username = "user", id = 1) + "\"efesfsef",
"something that is not even a token"
"something that is not even a token",
)
@ParameterizedTest(name = "[{index}] token `{0}` should be invalid")
+2 -2
View File
@@ -21,7 +21,7 @@ fun isRight() = object : Matcher<Either<*, *>> {
override fun invoke(actual: Either<*, *>) = when (actual) {
is Either.Right -> MatchResult.Match
is Either.Left -> {
val valueA = actual.a
val valueA = actual.value
MatchResult.Mismatch("is Either.Left<${if (valueA == null) "Null" else valueA::class.simpleName}>")
}
}
@@ -34,7 +34,7 @@ inline fun <reified A> isLeftOfType() = object : Matcher<Either<*, *>> {
override fun invoke(actual: Either<*, *>) = when (actual) {
is Either.Right -> MatchResult.Mismatch("was Either.Right<>")
is Either.Left -> {
val valueA = actual.a
val valueA = actual.value
if (valueA is A) MatchResult.Match
else MatchResult.Mismatch("was Left<${if (valueA == null) "Null" else valueA::class.simpleName}>")
}
@@ -1,65 +0,0 @@
package be.simplenotes.domain.usecases.users.login
import be.simplenotes.config.JwtConfig
import be.simplenotes.domain.security.BcryptPasswordHash
import be.simplenotes.domain.security.SimpleJwt
import be.simplenotes.domain.security.UserJwtMapper
import be.simplenotes.domain.testutils.isLeftOfType
import be.simplenotes.domain.testutils.isRight
import be.simplenotes.persistence.repositories.UserRepository
import be.simplenotes.types.PersistedUser
import com.natpryce.hamkrest.assertion.assertThat
import io.mockk.*
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import java.util.concurrent.TimeUnit
internal class LoginUseCaseImplTest {
// region setup
private val mockUserRepository = mockk<UserRepository>()
private val passwordHash = BcryptPasswordHash(test = true)
private val jwtConfig = JwtConfig("a secret", 1, TimeUnit.HOURS)
private val simpleJwt = SimpleJwt(jwtConfig, UserJwtMapper())
private val loginUseCase = LoginUseCaseImpl(mockUserRepository, passwordHash, simpleJwt)
@BeforeEach
fun resetMocks() {
clearMocks(mockUserRepository)
}
// endregion
@Test
fun `Login should fail with invalid form`() {
val form = LoginForm("", "a")
assertThat(loginUseCase.login(form), isLeftOfType<InvalidLoginForm>())
verify { mockUserRepository wasNot called }
}
@Test
fun `Login should fail with non existing user`() {
val form = LoginForm("someusername", "somepassword")
every { mockUserRepository.find(form.username!!) } returns null
assertThat(loginUseCase.login(form), isLeftOfType<Unregistered>())
}
@Test
fun `Login should fail with wrong password`() {
val form = LoginForm("someusername", "wrongpassword")
every { mockUserRepository.find(form.username!!) } returns
PersistedUser(form.username!!, passwordHash.crypt("right password"), 1)
assertThat(loginUseCase.login(form), isLeftOfType<WrongPassword>())
}
@Test
fun `Login should succeed with existing user and correct password`() {
val loginForm = LoginForm("someusername", "somepassword")
every { mockUserRepository.find(loginForm.username!!) } returns
PersistedUser(loginForm.username!!, passwordHash.crypt(loginForm.password!!), 1)
val res = loginUseCase.login(loginForm)
assertThat(res, isRight())
}
}
@@ -1,54 +0,0 @@
package be.simplenotes.domain.usecases.users.register
import be.simplenotes.domain.security.BcryptPasswordHash
import be.simplenotes.domain.testutils.isLeftOfType
import be.simplenotes.domain.testutils.isRight
import be.simplenotes.persistence.repositories.UserRepository
import be.simplenotes.persistence.transactions.TransactionService
import be.simplenotes.types.PersistedUser
import com.natpryce.hamkrest.assertion.assertThat
import com.natpryce.hamkrest.equalTo
import io.mockk.*
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
internal class RegisterUseCaseImplTest {
// region setup
private val mockUserRepository = mockk<UserRepository>()
private val passwordHash = BcryptPasswordHash(test = true)
private val noopTransactionService = object : TransactionService {
override fun <T> use(block: () -> T) = block()
}
private val registerUseCase = RegisterUseCaseImpl(mockUserRepository, passwordHash, noopTransactionService)
@BeforeEach
fun resetMocks() {
clearMocks(mockUserRepository)
}
// endregion
@Test
fun `register should fail with invalid form`() {
val form = RegisterForm("", "a".repeat(10))
assertThat(registerUseCase.register(form), isLeftOfType<InvalidRegisterForm>())
verify { mockUserRepository wasNot called }
}
@Test
fun `Register should fail with existing username`() {
val form = RegisterForm("someuser", "somepassword")
every { mockUserRepository.exists(form.username!!) } returns true
assertThat(registerUseCase.register(form), isLeftOfType<UserExists>())
}
@Test
fun `Register should succeed with new user`() {
val form = RegisterForm("someuser", "somepassword")
every { mockUserRepository.exists(form.username!!) } returns false
every { mockUserRepository.create(any()) } returns PersistedUser(form.username!!, form.password!!, 1)
val res = registerUseCase.register(form)
assertThat(res, isRight())
res.map { assertThat(it.username, equalTo(form.username)) }
}
}
@@ -1,4 +1,4 @@
package be.simplenotes.domain.usecases.search
package be.simplenotes.domain.utils
import be.simplenotes.search.SearchTerms
import com.natpryce.hamkrest.assertion.assertThat
@@ -33,7 +33,15 @@ internal class SearchTermsParserKtTest {
"tag:'example abc' title:'other with words' this is the end ",
title = "other with words",
tag = "example abc",
all = "this is the end"
all = "this is the end",
),
createResult("tag:blah", tag = "blah"),
createResult("tag:'some words'", tag = "some words"),
createResult("tag:'some words ' global", tag = "some words ", all = "global"),
createResult(
"tag:'double quote inside single \" ' global",
tag = "double quote inside single \" ",
all = "global",
),
)
@@ -1,10 +1,10 @@
package be.simplenotes.domain.validation
import be.simplenotes.domain.LoginError
import be.simplenotes.domain.LoginForm
import be.simplenotes.domain.RegisterForm
import be.simplenotes.domain.testutils.isLeftOfType
import be.simplenotes.domain.testutils.isRight
import be.simplenotes.domain.usecases.users.login.InvalidLoginForm
import be.simplenotes.domain.usecases.users.login.LoginForm
import be.simplenotes.domain.usecases.users.register.RegisterForm
import com.natpryce.hamkrest.assertion.assertThat
import org.junit.jupiter.api.Nested
import org.junit.jupiter.params.ParameterizedTest
@@ -22,19 +22,19 @@ internal class UserValidationsTest {
LoginForm(username = "", password = ""),
LoginForm(username = "a", password = "aaaa"),
LoginForm(username = "a".repeat(51), password = "a".repeat(8)),
LoginForm(username = "a".repeat(10), password = "a".repeat(7))
LoginForm(username = "a".repeat(10), password = "a".repeat(7)),
)
@ParameterizedTest
@MethodSource("invalidLoginForms")
fun `validate invalid logins`(form: LoginForm) {
assertThat(UserValidations.validateLogin(form), isLeftOfType<InvalidLoginForm>())
assertThat(UserValidations.validateLogin(form), isLeftOfType<LoginError.InvalidLoginForm>())
}
@Suppress("Unused")
fun validLoginForms(): Stream<LoginForm> = Stream.of(
LoginForm(username = "a".repeat(50), password = "a".repeat(72)),
LoginForm(username = "a".repeat(3), password = "a".repeat(8))
LoginForm(username = "a".repeat(3), password = "a".repeat(8)),
)
@ParameterizedTest
@@ -53,19 +53,19 @@ internal class UserValidationsTest {
RegisterForm(username = "", password = ""),
RegisterForm(username = "a", password = "aaaa"),
RegisterForm(username = "a".repeat(51), password = "a".repeat(8)),
RegisterForm(username = "a".repeat(10), password = "a".repeat(7))
RegisterForm(username = "a".repeat(10), password = "a".repeat(7)),
)
@ParameterizedTest
@MethodSource("invalidRegisterForms")
fun `validate invalid register`(form: LoginForm) {
assertThat(UserValidations.validateLogin(form), isLeftOfType<InvalidLoginForm>())
assertThat(UserValidations.validateLogin(form), isLeftOfType<LoginError.InvalidLoginForm>())
}
@Suppress("Unused")
fun validRegisterForms(): Stream<RegisterForm> = Stream.of(
RegisterForm(username = "a".repeat(50), password = "a".repeat(72)),
RegisterForm(username = "a".repeat(3), password = "a".repeat(8))
RegisterForm(username = "a".repeat(3), password = "a".repeat(8)),
)
@ParameterizedTest
+1 -1
View File
@@ -1,3 +1,3 @@
org.gradle.jvmargs=-Xmx2048M -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8
org.gradle.jvmargs=-Xmx2048M -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8
org.gradle.caching=true
org.gradle.parallel=true
+62
View File
@@ -0,0 +1,62 @@
[versions]
flexmark = "0.64.4"
lucene = "9.5.0"
http4k = "4.43.0.0"
jetty = "11.0.15"
mapstruct = "1.5.5.Final"
micronaut-inject = "4.0.0-M2"
[libraries]
flexmark-core = { module = "com.vladsch.flexmark:flexmark", version.ref = "flexmark" }
flexmark-tasklist = { module = "com.vladsch.flexmark:flexmark-ext-gfm-tasklist", version.ref = "flexmark" }
flyway = { module = "org.flywaydb:flyway-core", version = "9.17.0" }
hikariCP = { module = "com.zaxxer:HikariCP", version = "5.0.1" }
h2 = { module = "com.h2database:h2", version = "2.1.214" }
ktorm = { module = "org.ktorm:ktorm-core", version = "3.6.0" }
lucene-core = { module = "org.apache.lucene:lucene-core", version.ref = "lucene" }
lucene-analyzers-common = { module = "org.apache.lucene:lucene-analysis-common", version.ref = "lucene" }
lucene-queryparser = { module = "org.apache.lucene:lucene-queryparser", version.ref = "lucene" }
http4k-core = { module = "org.http4k:http4k-core", version.ref = "http4k" }
http4k-multipart = { module = "org.http4k:http4k-multipart", version.ref = "http4k" }
http4k-testing-hamkrest = { module = "org.http4k:http4k-testing-hamkrest", version.ref = "http4k" }
jetty-server = { module = "org.eclipse.jetty:jetty-server", version.ref = "jetty" }
jetty-servlet = { module = "org.eclipse.jetty:jetty-servlet", version.ref = "jetty" }
javax-servlet = { module = "jakarta.servlet:jakarta.servlet-api", version = "6.0.0" }
kotlinx-html = { module = "org.jetbrains.kotlinx:kotlinx-html-jvm", version = "0.8.1" }
kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json-jvm", version = "1.5.0" }
slf4j-api = { module = "org.slf4j:slf4j-api", version = "2.0.7" }
slf4j-logback = { module = "ch.qos.logback:logback-classic", version = "1.4.7" }
mapstruct-core = { module = "org.mapstruct:mapstruct", version.ref = "mapstruct" }
mapstruct-processor = { module = "org.mapstruct:mapstruct-processor", version.ref = "mapstruct" }
micronaut-inject = { module = "io.micronaut:micronaut-inject", version.ref = "micronaut-inject" }
micronaut-processor = { module = "io.micronaut:micronaut-inject-java", version.ref = "micronaut-inject" }
arrow-core = { module = "io.arrow-kt:arrow-core", version = "1.2.0-RC" }
commonsCompress = { module = "org.apache.commons:commons-compress", version = "1.23.0" }
jwt = { module = "com.auth0:java-jwt", version = "4.4.0" }
bcrypt = { module = "org.mindrot:jbcrypt", version = "0.4" }
konform = { module = "io.konform:konform-jvm", version = "0.4.0" }
owasp-html-sanitizer = { module = "com.googlecode.owasp-java-html-sanitizer:owasp-java-html-sanitizer", version = "20220608.1" }
prettytime = { module = "org.ocpsoft.prettytime:prettytime", version = "5.0.6.Final" }
yaml = { module = "org.yaml:snakeyaml", version = "2.0" }
assertJ = { module = "org.assertj:assertj-core", version = "3.24.2" }
hamkrest = { module = "com.natpryce:hamkrest", version = "1.8.0.1" }
junit-jupiter = { module = "org.junit.jupiter:junit-jupiter", version = "5.9.3" }
mockk = { module = "io.mockk:mockk", version = "1.13.5" }
faker = { module = "com.github.javafaker:javafaker", version = "1.0.2" }
[bundles]
flexmark = ["flexmark-core", "flexmark-tasklist"]
database = ["flyway", "hikariCP", "h2", "ktorm"]
lucene = ["lucene-core", "lucene-analyzers-common", "lucene-queryparser"]
jetty = ["jetty-server", "jetty-servlet", "javax-servlet"]
test = ["assertJ", "hamkrest", "junit-jupiter", "mockk"]
Binary file not shown.
+2 -1
View File
@@ -1,5 +1,6 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-6.8.3-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.1.1-bin.zip
networkTimeout=10000
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
+11 -22
View File
@@ -1,4 +1,3 @@
import be.simplenotes.Libs
import be.simplenotes.micronaut
plugins {
@@ -12,39 +11,29 @@ dependencies {
implementation(project(":types"))
implementation(project(":config"))
implementation(Libs.Database.Drivers.mariadb)
implementation(Libs.Database.Drivers.h2)
implementation(Libs.Database.flyway)
implementation(Libs.Database.hikariCP)
implementation(Libs.Database.Ktorm.core)
runtimeOnly(Libs.Database.Ktorm.mysql)
implementation(libs.bundles.database)
implementation(Libs.Slf4J.api)
runtimeOnly(Libs.Slf4J.logback)
implementation(libs.slf4j.api)
runtimeOnly(libs.slf4j.logback)
compileOnly(Libs.Mapstruct.core)
kapt(Libs.Mapstruct.processor)
compileOnly(libs.mapstruct.core)
kapt(libs.mapstruct.processor)
testImplementation(Libs.Test.junit)
testImplementation(Libs.Test.assertJ)
testCompileOnly(Libs.Slf4J.logback)
testImplementation(Libs.Test.mariaTestContainer)
testImplementation(libs.bundles.test)
testCompileOnly(libs.slf4j.logback)
testFixturesImplementation(project(":types"))
testFixturesImplementation(project(":config"))
testFixturesImplementation(project(":persistence"))
testFixturesImplementation(Libs.Test.faker) {
testFixturesImplementation(libs.faker) {
exclude(group = "org.yaml")
}
testFixturesImplementation(Libs.snakeyaml)
testFixturesImplementation(libs.yaml)
testFixturesImplementation(Libs.Test.mariaTestContainer)
testFixturesImplementation(Libs.Database.flyway)
testFixturesImplementation(Libs.Test.junit)
testFixturesImplementation(Libs.Database.Ktorm.core)
testFixturesImplementation(Libs.Database.hikariCP)
testFixturesImplementation(libs.bundles.database)
testFixturesImplementation(libs.junit.jupiter)
micronaut()
@@ -9,7 +9,7 @@ create table Users
create table Notes
(
uuid binary(16) not null primary key,
uuid uuid not null primary key,
title varchar(50) not null,
markdown mediumtext not null,
html mediumtext not null,
@@ -17,7 +17,6 @@ create table Notes
updated_at datetime null,
constraint Notes_fk_user foreign key (user_id) references Users (id) on delete cascade
);
create index user_id on Notes (user_id);
@@ -26,7 +25,7 @@ create table Tags
(
id int auto_increment primary key,
name varchar(50) not null,
note_uuid binary(16) not null,
note_uuid uuid not null,
constraint Tags_fk_note foreign key (note_uuid) references Notes (uuid) on delete cascade
);
@@ -0,0 +1,5 @@
CREATE VIEW NotesWithTags AS
SELECT n.*, coalesce(ARRAY_AGG(t.NAME), ARRAY[]) as tags
FROM Notes n
LEFT JOIN Tags t ON n.uuid = t.note_uuid
GROUP BY n.uuid
@@ -1,36 +0,0 @@
create table Users
(
id int auto_increment primary key,
username varchar(50) not null,
password varchar(255) not null,
constraint username unique (username)
) character set 'utf8mb4'
collate 'utf8mb4_general_ci';
create table Notes
(
uuid binary(16) not null primary key,
title varchar(50) not null,
markdown mediumtext not null,
html mediumtext not null,
user_id int not null,
updated_at datetime null,
constraint Notes_fk_user foreign key (user_id) references Users (id) on delete cascade
) character set 'utf8mb4'
collate 'utf8mb4_general_ci';
create index user_id on Notes (user_id);
create table Tags
(
id int auto_increment primary key,
name varchar(50) not null,
note_uuid binary(16) not null,
constraint Tags_fk_note foreign key (note_uuid) references Notes (uuid) on delete cascade
) character set 'utf8mb4'
collate 'utf8mb4_general_ci';
create index note_uuid on Tags (note_uuid);
@@ -1,2 +0,0 @@
alter table Notes
add column deleted bool not null default false
@@ -1,2 +0,0 @@
alter table Notes
add column public bool not null default false
+55
View File
@@ -0,0 +1,55 @@
package be.simplenotes.persistence
import org.ktorm.database.Database
import org.ktorm.entity.Entity
import org.ktorm.entity.sequenceOf
import java.time.LocalDateTime
import java.util.*
internal val Database.users get() = sequenceOf(Users, withReferences = false)
internal val Database.notes get() = sequenceOf(Notes, withReferences = false)
internal val Database.tags get() = sequenceOf(Tags, withReferences = false)
internal val Database.noteWithTags get() = sequenceOf(NotesWithTags, withReferences = false)
internal interface UserEntity : Entity<UserEntity> {
companion object : Entity.Factory<UserEntity>()
var id: Int
var username: String
var password: String
}
internal interface NoteEntity : Entity<NoteEntity> {
companion object : Entity.Factory<NoteEntity>()
var uuid: UUID
var title: String
var markdown: String
var html: String
var updatedAt: LocalDateTime
var deleted: Boolean
var public: Boolean
var user: UserEntity
}
internal interface TagEntity : Entity<TagEntity> {
companion object : Entity.Factory<TagEntity>()
val id: Int
var name: String
var note: NoteEntity
}
internal interface NoteWithTagsEntity : Entity<NoteWithTagsEntity> {
companion object : Entity.Factory<NoteWithTagsEntity>()
var uuid: UUID
var title: String
var markdown: String
var html: String
var updatedAt: LocalDateTime
var deleted: Boolean
var public: Boolean
var user: UserEntity
val tags: List<String>
}
-31
View File
@@ -1,31 +0,0 @@
package be.simplenotes.persistence
import be.simplenotes.config.DataSourceConfig
import be.simplenotes.persistence.utils.DbType
import be.simplenotes.persistence.utils.type
import me.liuwj.ktorm.database.Database
import me.liuwj.ktorm.database.asIterable
import me.liuwj.ktorm.database.use
import java.sql.SQLException
import javax.inject.Singleton
interface DbHealthCheck {
fun isOk(): Boolean
}
@Singleton
internal class DbHealthCheckImpl(
private val db: Database,
private val dataSourceConfig: DataSourceConfig,
) : DbHealthCheck {
override fun isOk() = if (dataSourceConfig.type() == DbType.H2) true
else try {
db.useConnection { connection ->
connection.prepareStatement("""SHOW DATABASES""").use {
it.executeQuery().asIterable().map { it.getString(1) }
}
}.any { it in dataSourceConfig.jdbcUrl }
} catch (e: SQLException) {
false
}
}
+4 -15
View File
@@ -1,10 +1,7 @@
package be.simplenotes.persistence
import be.simplenotes.config.DataSourceConfig
import be.simplenotes.persistence.utils.DbType
import be.simplenotes.persistence.utils.type
import org.flywaydb.core.Flyway
import javax.inject.Singleton
import jakarta.inject.Singleton
import javax.sql.DataSource
interface DbMigrations {
@@ -12,20 +9,12 @@ interface DbMigrations {
}
@Singleton
internal class DbMigrationsImpl(
private val dataSource: DataSource,
private val dataSourceConfig: DataSourceConfig,
) : DbMigrations {
internal class DbMigrationsImpl(private val dataSource: DataSource) : DbMigrations {
override fun migrate() {
val migrationDir = when (dataSourceConfig.type()) {
DbType.H2 -> "db/migration/other"
DbType.MariaDb -> "db/migration/mariadb"
}
Flyway.configure()
.dataSource(dataSource)
.locations(migrationDir)
.locations("db/migration")
.loggers("slf4j")
.load()
.migrate()
}
+15 -6
View File
@@ -1,12 +1,14 @@
package be.simplenotes.persistence
import be.simplenotes.config.DataSourceConfig
import be.simplenotes.persistence.extensions.CustomSqlFormatter
import com.zaxxer.hikari.HikariConfig
import com.zaxxer.hikari.HikariDataSource
import io.micronaut.context.annotation.Bean
import io.micronaut.context.annotation.Factory
import me.liuwj.ktorm.database.Database
import javax.inject.Singleton
import org.ktorm.database.Database
import org.ktorm.database.SqlDialect
import jakarta.inject.Singleton
import javax.sql.DataSource
@Factory
@@ -15,7 +17,13 @@ class PersistenceModule {
@Singleton
internal fun database(migrations: DbMigrations, dataSource: DataSource): Database {
migrations.migrate()
return Database.connect(dataSource)
return Database.connect(
dataSource,
dialect = object : SqlDialect {
override fun createSqlFormatter(database: Database, beautifySql: Boolean, indentSize: Int) =
CustomSqlFormatter(database, beautifySql, indentSize)
},
)
}
@Singleton
@@ -23,11 +31,12 @@ class PersistenceModule {
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.driverClassName = "org.h2.Driver"
it.username = ""
it.password = ""
it.maximumPoolSize = conf.maximumPoolSize
it.connectionTimeout = conf.connectionTimeout
it.dataSourceProperties["CASE_INSENSITIVE_IDENTIFIERS"] = "TRUE"
}
return HikariDataSource(hikariConfig)
}

Some files were not shown because too many files have changed in this diff Show More