Compare commits
18 Commits
90701dcdce
..
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 195c7c10ac | |||
| 724fa0483e | |||
| f2bdc8d6c7 | |||
| 5aa2e80c5f | |||
| 235e8b6e3c | |||
| 1e0fe12396 | |||
| 7ad8b7039b | |||
| 204ae7988e | |||
| a4bf998c5b | |||
| 3e1683dfe5 | |||
| 51b682c593 | |||
| ea110d51d3 | |||
| f255064533 | |||
| 761382da23 | |||
| 525e3a4a3f | |||
| 69e50b158f | |||
| 8997433974 | |||
| 909fb482a8 |
+4
-3
@@ -9,9 +9,10 @@ charset = utf-8
|
|||||||
trim_trailing_whitespace = true
|
trim_trailing_whitespace = true
|
||||||
insert_final_newline = true
|
insert_final_newline = true
|
||||||
|
|
||||||
[*.{kt, kts}]
|
[*.{kt,kts}]
|
||||||
indent_size = 4
|
indent_size = 4
|
||||||
insert_final_newline = true
|
insert_final_newline = true
|
||||||
max_line_length = 120
|
max_line_length = 120
|
||||||
disabled_rules = no-wildcard-imports
|
ktlint_standard_no-wildcard-imports = disabled
|
||||||
kotlin_imports_layout = idea
|
ktlint_standard_import-ordering = disabled
|
||||||
|
ktlint_standard_multiline-if-else = disabled
|
||||||
|
|||||||
@@ -1,6 +1 @@
|
|||||||
# mariadb
|
|
||||||
MYSQL_ROOT_PASSWORD=
|
|
||||||
MYSQL_PASSWORD=
|
|
||||||
# simplenotes
|
|
||||||
DB_PASSWORD=
|
|
||||||
JWT_SECRET=
|
JWT_SECRET=
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# Enable auto-env through the sdkman_auto_env config
|
# Enable auto-env through the sdkman_auto_env config
|
||||||
# Add key=value pairs of SDKs to use below
|
# Add key=value pairs of SDKs to use below
|
||||||
java=14.0.2-open
|
java=15-open
|
||||||
gradle=6.7
|
gradle=6.8-rc-1
|
||||||
kotlin=1.4.10
|
kotlin=1.4.20
|
||||||
|
|||||||
+7
-8
@@ -1,8 +1,8 @@
|
|||||||
FROM openjdk:14-alpine as jdkbuilder
|
FROM eclipse-temurin:19-alpine as jdkbuilder
|
||||||
|
|
||||||
RUN apk add --no-cache binutils
|
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
|
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
|
RUN apk add --no-cache curl
|
||||||
|
|
||||||
ENV APPLICATION_USER simplenotes
|
|
||||||
RUN adduser -D -g '' $APPLICATION_USER
|
|
||||||
|
|
||||||
RUN mkdir /app
|
RUN mkdir /app
|
||||||
RUN chown -R $APPLICATION_USER /app
|
RUN mkdir /app/data
|
||||||
|
|
||||||
USER $APPLICATION_USER
|
|
||||||
|
|
||||||
COPY --from=jdkbuilder /myjdk /myjdk
|
COPY --from=jdkbuilder /myjdk /myjdk
|
||||||
COPY app/build/libs/app-with-dependencies*.jar /app/simplenotes.jar
|
COPY app/build/libs/app-with-dependencies*.jar /app/simplenotes.jar
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
|
VOLUME /app/data
|
||||||
|
|
||||||
|
ENV SERVER_HOST 0.0.0.0
|
||||||
|
|
||||||
CMD [ \
|
CMD [ \
|
||||||
"/myjdk/bin/java", \
|
"/myjdk/bin/java", \
|
||||||
"--add-opens", \
|
"--add-opens", \
|
||||||
|
|||||||
+19
-19
@@ -1,33 +1,33 @@
|
|||||||
import be.simplenotes.Libs
|
import be.simplenotes.micronaut
|
||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
id("be.simplenotes.base")
|
id("be.simplenotes.base")
|
||||||
id("be.simplenotes.kotlinx-serialization")
|
id("be.simplenotes.kotlinx-serialization")
|
||||||
id("be.simplenotes.app-shadow")
|
id("be.simplenotes.app-shadow")
|
||||||
id("be.simplenotes.app-css")
|
id("be.simplenotes.docker")
|
||||||
id("be.simplenotes.app-docker")
|
id("be.simplenotes.micronaut")
|
||||||
kotlin("kapt")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation(project(":domain"))
|
implementation(project(":domain"))
|
||||||
implementation(project(":types"))
|
|
||||||
implementation(project(":config"))
|
|
||||||
implementation(project(":views"))
|
implementation(project(":views"))
|
||||||
|
implementation(project(":css"))
|
||||||
|
|
||||||
implementation(Libs.arrowCoreData)
|
implementation(libs.http4k.core)
|
||||||
implementation(Libs.konform)
|
implementation(libs.http4k.multipart)
|
||||||
implementation(Libs.http4kCore)
|
implementation(libs.bundles.jetty)
|
||||||
implementation(Libs.jettyServer)
|
implementation(libs.kotlinx.serialization.json)
|
||||||
implementation(Libs.jettyServlet)
|
|
||||||
implementation(Libs.javaxServlet)
|
|
||||||
implementation(Libs.kotlinxSerializationJson)
|
|
||||||
implementation(Libs.logbackClassic)
|
|
||||||
|
|
||||||
implementation(Libs.micronaut)
|
implementation(libs.slf4j.api)
|
||||||
kapt(Libs.micronautProcessor)
|
runtimeOnly(libs.slf4j.logback)
|
||||||
|
|
||||||
testImplementation(Libs.junit)
|
micronaut()
|
||||||
testImplementation(Libs.assertJ)
|
|
||||||
testImplementation(Libs.http4kTestingHamkrest)
|
testImplementation(libs.bundles.test)
|
||||||
|
testImplementation(libs.http4k.testing.hamkrest)
|
||||||
|
}
|
||||||
|
|
||||||
|
docker {
|
||||||
|
image = "hubv/simplenotes"
|
||||||
|
tag = "latest"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
<configuration>
|
<configuration>
|
||||||
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
|
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
|
||||||
<withJansi>true</withJansi>
|
|
||||||
<encoder>
|
<encoder>
|
||||||
<pattern>%cyan(%d{YYYY-MM-dd HH:mm:ss.SSS}) [%thread] %highlight(%-5level) %green(%logger{36}) - %msg%n
|
<pattern>%cyan(%d{YYYY-MM-dd HH:mm:ss.SSS}) [%thread] %highlight(%-5level) %green(%logger{36}) - %msg%n
|
||||||
</pattern>
|
</pattern>
|
||||||
|
|||||||
+3
-3
@@ -1,10 +1,10 @@
|
|||||||
package be.simplenotes.app
|
package be.simplenotes.app
|
||||||
|
|
||||||
|
import jakarta.annotation.PostConstruct
|
||||||
|
import jakarta.annotation.PreDestroy
|
||||||
|
import jakarta.inject.Singleton
|
||||||
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 javax.inject.Singleton
|
|
||||||
import be.simplenotes.config.ServerConfig as SimpleNotesServerConfig
|
import be.simplenotes.config.ServerConfig as SimpleNotesServerConfig
|
||||||
|
|
||||||
@Singleton
|
@Singleton
|
||||||
|
|||||||
@@ -4,7 +4,11 @@ import io.micronaut.context.ApplicationContext
|
|||||||
import java.lang.Runtime.getRuntime
|
import java.lang.Runtime.getRuntime
|
||||||
|
|
||||||
fun main() {
|
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)
|
ctx.createBean(Server::class.java)
|
||||||
getRuntime().addShutdownHook(Thread { ctx.stop() })
|
getRuntime().addShutdownHook(Thread { ctx.stop() })
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
package be.simplenotes.app.api
|
package be.simplenotes.app.api
|
||||||
|
|
||||||
import be.simplenotes.app.extensions.auto
|
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.LoggedInUser
|
||||||
import be.simplenotes.types.PersistedNote
|
import be.simplenotes.types.PersistedNote
|
||||||
import be.simplenotes.types.PersistedNoteMetadata
|
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.Path
|
||||||
import org.http4k.lens.uuid
|
import org.http4k.lens.uuid
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import javax.inject.Singleton
|
import jakarta.inject.Singleton
|
||||||
|
|
||||||
@Singleton
|
@Singleton
|
||||||
class ApiNoteController(
|
class ApiNoteController(
|
||||||
@@ -28,7 +28,7 @@ class ApiNoteController(
|
|||||||
val content = noteContentLens(request)
|
val content = noteContentLens(request)
|
||||||
return noteService.create(loggedInUser, content).fold(
|
return noteService.create(loggedInUser, content).fold(
|
||||||
{ Response(BAD_REQUEST) },
|
{ 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)
|
if (it == null) Response(NOT_FOUND)
|
||||||
else Response(OK)
|
else Response(OK)
|
||||||
}
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -76,4 +76,4 @@ data class NoteContent(val content: String)
|
|||||||
data class UuidContent(@Contextual val uuid: UUID)
|
data class UuidContent(@Contextual val uuid: UUID)
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class SearchContent(@Contextual val query: String)
|
data class SearchContent(val query: String)
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
package be.simplenotes.app.api
|
package be.simplenotes.app.api
|
||||||
|
|
||||||
import be.simplenotes.app.extensions.auto
|
import be.simplenotes.app.extensions.auto
|
||||||
import be.simplenotes.domain.usecases.UserService
|
import be.simplenotes.domain.LoginForm
|
||||||
import be.simplenotes.domain.usecases.users.login.LoginForm
|
import be.simplenotes.domain.UserService
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
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.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
|
import jakarta.inject.Singleton
|
||||||
|
|
||||||
@Singleton
|
@Singleton
|
||||||
class ApiUserController(
|
class ApiUserController(
|
||||||
@@ -23,7 +23,7 @@ class ApiUserController(
|
|||||||
.login(loginFormLens(request))
|
.login(loginFormLens(request))
|
||||||
.fold(
|
.fold(
|
||||||
{ Response(BAD_REQUEST) },
|
{ Response(BAD_REQUEST) },
|
||||||
{ tokenLens(Token(it), Response(OK)) }
|
{ tokenLens(Token(it), Response(OK)) },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ 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
|
import jakarta.inject.Singleton
|
||||||
|
|
||||||
@Singleton
|
@Singleton
|
||||||
class BaseController(private val view: BaseView) {
|
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)
|
|
||||||
}
|
|
||||||
@@ -2,10 +2,8 @@ package be.simplenotes.app.controllers
|
|||||||
|
|
||||||
import be.simplenotes.app.extensions.html
|
import be.simplenotes.app.extensions.html
|
||||||
import be.simplenotes.app.extensions.redirect
|
import be.simplenotes.app.extensions.redirect
|
||||||
import be.simplenotes.domain.usecases.NoteService
|
import be.simplenotes.domain.MarkdownParsingError
|
||||||
import be.simplenotes.domain.usecases.markdown.InvalidMeta
|
import be.simplenotes.domain.NoteService
|
||||||
import be.simplenotes.domain.usecases.markdown.MissingMeta
|
|
||||||
import be.simplenotes.domain.usecases.markdown.ValidationError
|
|
||||||
import be.simplenotes.types.LoggedInUser
|
import be.simplenotes.types.LoggedInUser
|
||||||
import be.simplenotes.views.NoteView
|
import be.simplenotes.views.NoteView
|
||||||
import org.http4k.core.Method
|
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.core.body.form
|
||||||
import org.http4k.routing.path
|
import org.http4k.routing.path
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import javax.inject.Singleton
|
import jakarta.inject.Singleton
|
||||||
import kotlin.math.abs
|
import kotlin.math.abs
|
||||||
|
|
||||||
@Singleton
|
@Singleton
|
||||||
@@ -34,27 +32,27 @@ class NoteController(
|
|||||||
return noteService.create(loggedInUser, markdownForm).fold(
|
return noteService.create(loggedInUser, markdownForm).fold(
|
||||||
{
|
{
|
||||||
val html = when (it) {
|
val html = when (it) {
|
||||||
MissingMeta -> view.noteEditor(
|
MarkdownParsingError.MissingMeta -> view.noteEditor(
|
||||||
loggedInUser,
|
loggedInUser,
|
||||||
error = "Missing note metadata",
|
error = "Missing note metadata",
|
||||||
textarea = markdownForm
|
textarea = markdownForm,
|
||||||
)
|
)
|
||||||
InvalidMeta -> view.noteEditor(
|
MarkdownParsingError.InvalidMeta -> view.noteEditor(
|
||||||
loggedInUser,
|
loggedInUser,
|
||||||
error = "Invalid note metadata",
|
error = "Invalid note metadata",
|
||||||
textarea = markdownForm
|
textarea = markdownForm,
|
||||||
)
|
)
|
||||||
is ValidationError -> view.noteEditor(
|
is MarkdownParsingError.ValidationError -> view.noteEditor(
|
||||||
loggedInUser,
|
loggedInUser,
|
||||||
validationErrors = it.validationErrors,
|
validationErrors = it.validationErrors,
|
||||||
textarea = markdownForm
|
textarea = markdownForm,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
Response(BAD_REQUEST).html(html)
|
Response(BAD_REQUEST).html(html)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Response.redirect("/notes/${it.uuid}")
|
Response.redirect("/notes/${it.uuid}")
|
||||||
}
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -113,27 +111,27 @@ class NoteController(
|
|||||||
return noteService.update(loggedInUser, note.uuid, markdownForm).fold(
|
return noteService.update(loggedInUser, note.uuid, markdownForm).fold(
|
||||||
{
|
{
|
||||||
val html = when (it) {
|
val html = when (it) {
|
||||||
MissingMeta -> view.noteEditor(
|
MarkdownParsingError.MissingMeta -> view.noteEditor(
|
||||||
loggedInUser,
|
loggedInUser,
|
||||||
error = "Missing note metadata",
|
error = "Missing note metadata",
|
||||||
textarea = markdownForm
|
textarea = markdownForm,
|
||||||
)
|
)
|
||||||
InvalidMeta -> view.noteEditor(
|
MarkdownParsingError.InvalidMeta -> view.noteEditor(
|
||||||
loggedInUser,
|
loggedInUser,
|
||||||
error = "Invalid note metadata",
|
error = "Invalid note metadata",
|
||||||
textarea = markdownForm
|
textarea = markdownForm,
|
||||||
)
|
)
|
||||||
is ValidationError -> view.noteEditor(
|
is MarkdownParsingError.ValidationError -> view.noteEditor(
|
||||||
loggedInUser,
|
loggedInUser,
|
||||||
validationErrors = it.validationErrors,
|
validationErrors = it.validationErrors,
|
||||||
textarea = markdownForm
|
textarea = markdownForm,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
Response(BAD_REQUEST).html(html)
|
Response(BAD_REQUEST).html(html)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Response.redirect("/notes/${note.uuid}")
|
Response.redirect("/notes/${note.uuid}")
|
||||||
}
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,19 +2,19 @@ package be.simplenotes.app.controllers
|
|||||||
|
|
||||||
import be.simplenotes.app.extensions.html
|
import be.simplenotes.app.extensions.html
|
||||||
import be.simplenotes.app.extensions.redirect
|
import be.simplenotes.app.extensions.redirect
|
||||||
import be.simplenotes.domain.usecases.UserService
|
import be.simplenotes.domain.*
|
||||||
import be.simplenotes.domain.usecases.users.delete.DeleteError
|
|
||||||
import be.simplenotes.domain.usecases.users.delete.DeleteForm
|
|
||||||
import be.simplenotes.types.LoggedInUser
|
import be.simplenotes.types.LoggedInUser
|
||||||
import be.simplenotes.views.SettingView
|
import be.simplenotes.views.SettingView
|
||||||
|
import jakarta.inject.Singleton
|
||||||
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
|
@Singleton
|
||||||
class SettingsController(
|
class SettingsController(
|
||||||
private val userService: UserService,
|
private val userService: UserService,
|
||||||
|
private val exportService: ExportService,
|
||||||
|
private val importService: ImportService,
|
||||||
private val settingView: SettingView,
|
private val settingView: SettingView,
|
||||||
) {
|
) {
|
||||||
fun settings(request: Request, loggedInUser: LoggedInUser): Response {
|
fun settings(request: Request, loggedInUser: LoggedInUser): Response {
|
||||||
@@ -31,20 +31,21 @@ class SettingsController(
|
|||||||
DeleteError.WrongPassword -> Response(Status.OK).html(
|
DeleteError.WrongPassword -> Response(Status.OK).html(
|
||||||
settingView.settings(
|
settingView.settings(
|
||||||
loggedInUser,
|
loggedInUser,
|
||||||
error = "Wrong password"
|
error = "Wrong password",
|
||||||
)
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
is DeleteError.InvalidForm -> Response(Status.OK).html(
|
is DeleteError.InvalidForm -> Response(Status.OK).html(
|
||||||
settingView.settings(
|
settingView.settings(
|
||||||
loggedInUser,
|
loggedInUser,
|
||||||
validationErrors = it.validationErrors
|
validationErrors = it.validationErrors,
|
||||||
)
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Response.redirect("/").invalidateCookie("Bearer")
|
Response.redirect("/").invalidateCookie("Bearer")
|
||||||
}
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -61,20 +62,28 @@ class SettingsController(
|
|||||||
return if (isDownload) {
|
return if (isDownload) {
|
||||||
val filename = "simplenotes-export-${loggedInUser.username}"
|
val filename = "simplenotes-export-${loggedInUser.username}"
|
||||||
if (request.form("format") == "zip") {
|
if (request.form("format") == "zip") {
|
||||||
val zip = userService.exportAsZip(loggedInUser.userId)
|
val zip = exportService.exportAsZip(loggedInUser.userId)
|
||||||
Response(Status.OK)
|
Response(Status.OK)
|
||||||
.with(attachment("$filename.zip", "application/zip"))
|
.with(attachment("$filename.zip", "application/zip"))
|
||||||
.body(zip)
|
.body(zip)
|
||||||
} else
|
} else
|
||||||
Response(Status.OK)
|
Response(Status.OK)
|
||||||
.with(attachment("$filename.json", "application/json"))
|
.with(attachment("$filename.json", "application/json"))
|
||||||
.body(userService.exportAsJson(loggedInUser.userId))
|
.body(exportService.exportAsJson(loggedInUser.userId))
|
||||||
} else Response(Status.OK).body(userService.exportAsJson(loggedInUser.userId)).header(
|
} else Response(Status.OK).body(exportService.exportAsJson(loggedInUser.userId)).header(
|
||||||
"Content-Type",
|
"Content-Type",
|
||||||
"application/json"
|
"application/json",
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun Request.deleteForm(loggedInUser: LoggedInUser) =
|
private fun Request.deleteForm(loggedInUser: LoggedInUser) =
|
||||||
DeleteForm(loggedInUser.username, form("password"), form("checked") != null)
|
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")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,11 +4,7 @@ import be.simplenotes.app.extensions.html
|
|||||||
import be.simplenotes.app.extensions.isSecure
|
import be.simplenotes.app.extensions.isSecure
|
||||||
import be.simplenotes.app.extensions.redirect
|
import be.simplenotes.app.extensions.redirect
|
||||||
import be.simplenotes.config.JwtConfig
|
import be.simplenotes.config.JwtConfig
|
||||||
import be.simplenotes.domain.usecases.UserService
|
import be.simplenotes.domain.*
|
||||||
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.types.LoggedInUser
|
import be.simplenotes.types.LoggedInUser
|
||||||
import be.simplenotes.views.UserView
|
import be.simplenotes.views.UserView
|
||||||
import org.http4k.core.Method.GET
|
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.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
|
import jakarta.inject.Singleton
|
||||||
|
|
||||||
@Singleton
|
@Singleton
|
||||||
class UserController(
|
class UserController(
|
||||||
@@ -31,7 +27,7 @@ class UserController(
|
|||||||
) {
|
) {
|
||||||
fun register(request: Request, loggedInUser: LoggedInUser?): Response {
|
fun register(request: Request, loggedInUser: LoggedInUser?): Response {
|
||||||
if (request.method == GET) return Response(OK).html(
|
if (request.method == GET) return Response(OK).html(
|
||||||
userView.register(loggedInUser)
|
userView.register(loggedInUser),
|
||||||
)
|
)
|
||||||
|
|
||||||
val result = userService.register(request.registerForm())
|
val result = userService.register(request.registerForm())
|
||||||
@@ -39,21 +35,21 @@ class UserController(
|
|||||||
return result.fold(
|
return result.fold(
|
||||||
{
|
{
|
||||||
val html = when (it) {
|
val html = when (it) {
|
||||||
UserExists -> userView.register(
|
RegisterError.UserExists -> userView.register(
|
||||||
loggedInUser,
|
loggedInUser,
|
||||||
error = "User already exists"
|
error = "User already exists",
|
||||||
)
|
)
|
||||||
is InvalidRegisterForm ->
|
is RegisterError.InvalidRegisterForm ->
|
||||||
userView.register(
|
userView.register(
|
||||||
loggedInUser,
|
loggedInUser,
|
||||||
validationErrors = it.validationErrors
|
validationErrors = it.validationErrors,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
Response(OK).html(html)
|
Response(OK).html(html)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Response.redirect("/login")
|
Response.redirect("/login")
|
||||||
}
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -62,7 +58,7 @@ class UserController(
|
|||||||
|
|
||||||
fun login(request: Request, loggedInUser: LoggedInUser?): Response {
|
fun login(request: Request, loggedInUser: LoggedInUser?): Response {
|
||||||
if (request.method == GET) return Response(OK).html(
|
if (request.method == GET) return Response(OK).html(
|
||||||
userView.login(loggedInUser)
|
userView.login(loggedInUser),
|
||||||
)
|
)
|
||||||
|
|
||||||
val result = userService.login(request.loginForm())
|
val result = userService.login(request.loginForm())
|
||||||
@@ -70,27 +66,27 @@ class UserController(
|
|||||||
return result.fold(
|
return result.fold(
|
||||||
{
|
{
|
||||||
val html = when (it) {
|
val html = when (it) {
|
||||||
Unregistered ->
|
LoginError.Unregistered ->
|
||||||
userView.login(
|
userView.login(
|
||||||
loggedInUser,
|
loggedInUser,
|
||||||
error = "User does not exist"
|
error = "User does not exist",
|
||||||
)
|
)
|
||||||
WrongPassword ->
|
LoginError.WrongPassword ->
|
||||||
userView.login(
|
userView.login(
|
||||||
loggedInUser,
|
loggedInUser,
|
||||||
error = "Wrong password"
|
error = "Wrong password",
|
||||||
)
|
)
|
||||||
is InvalidLoginForm ->
|
is LoginError.InvalidLoginForm ->
|
||||||
userView.login(
|
userView.login(
|
||||||
loggedInUser,
|
loggedInUser,
|
||||||
validationErrors = it.validationErrors
|
validationErrors = it.validationErrors,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
Response(OK).html(html)
|
Response(OK).html(html)
|
||||||
},
|
},
|
||||||
{ token ->
|
{ token ->
|
||||||
Response.redirect("/notes").loginCookie(token, request.isSecure())
|
Response.redirect("/notes").loginCookie(token, request.isSecure())
|
||||||
}
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -105,8 +101,8 @@ class UserController(
|
|||||||
httpOnly = true,
|
httpOnly = true,
|
||||||
sameSite = SameSite.Lax,
|
sameSite = SameSite.Lax,
|
||||||
maxAge = validityInSeconds,
|
maxAge = validityInSeconds,
|
||||||
secure = secure
|
secure = secure,
|
||||||
)
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -24,13 +24,13 @@ fun Request.isSecure() = header("X-Forwarded-Proto")?.contains("https") ?: false
|
|||||||
val bodyLens = httpBodyRoot(
|
val bodyLens = httpBodyRoot(
|
||||||
listOf(Meta(true, "body", ParamMeta.ObjectParam, "body")),
|
listOf(Meta(true, "body", ParamMeta.ObjectParam, "body")),
|
||||||
ContentType.APPLICATION_JSON.withNoDirectives(),
|
ContentType.APPLICATION_JSON.withNoDirectives(),
|
||||||
ContentNegotiation.StrictNoDirective
|
ContentNegotiation.StrictNoDirective,
|
||||||
).map(
|
).map(
|
||||||
{ it.payload.asString() },
|
{ it.payload.asString() },
|
||||||
{ Body(it) }
|
{ Body(it) },
|
||||||
)
|
)
|
||||||
|
|
||||||
inline fun <reified T> Json.auto(): BiDiBodyLensSpec<T> = bodyLens.map(
|
inline fun <reified T> Json.auto(): BiDiBodyLensSpec<T> = bodyLens.map(
|
||||||
{ decodeFromString(it) },
|
{ decodeFromString(it) },
|
||||||
{ encodeToString(it) }
|
{ encodeToString(it) },
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ 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
|
import jakarta.inject.Singleton
|
||||||
|
|
||||||
@Singleton
|
@Singleton
|
||||||
class ErrorFilter(private val errorView: ErrorView) : Filter {
|
class ErrorFilter(private val errorView: ErrorView) : Filter {
|
||||||
|
|||||||
@@ -8,19 +8,18 @@ import org.eclipse.jetty.servlet.ServletHolder
|
|||||||
import org.http4k.core.HttpHandler
|
import org.http4k.core.HttpHandler
|
||||||
import org.http4k.server.Http4kServer
|
import org.http4k.server.Http4kServer
|
||||||
import org.http4k.server.ServerConfig
|
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 {
|
class Jetty(private val port: Int, private val server: Server) : ServerConfig {
|
||||||
constructor(port: Int = 8000) : this(port, http(port))
|
|
||||||
constructor(port: Int, vararg inConnectors: ConnectorBuilder) : this(
|
constructor(port: Int, vararg inConnectors: ConnectorBuilder) : this(
|
||||||
port,
|
port,
|
||||||
Server().apply {
|
Server().apply {
|
||||||
inConnectors.forEach { addConnector(it(this)) }
|
inConnectors.forEach { addConnector(it(this)) }
|
||||||
}
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
override fun toServer(httpHandler: HttpHandler): Http4kServer {
|
override fun toServer(http: HttpHandler): Http4kServer {
|
||||||
server.insertHandler(httpHandler.toJettyHandler())
|
server.insertHandler(http.toJettyHandler())
|
||||||
|
|
||||||
return object : Http4kServer {
|
return object : Http4kServer {
|
||||||
override fun start(): Http4kServer = apply {
|
override fun start(): Http4kServer = apply {
|
||||||
@@ -39,5 +38,3 @@ fun HttpHandler.toJettyHandler() = ServletContextHandler(SESSIONS).apply {
|
|||||||
}
|
}
|
||||||
|
|
||||||
typealias ConnectorBuilder = (Server) -> ServerConnector
|
typealias ConnectorBuilder = (Server) -> ServerConnector
|
||||||
|
|
||||||
fun http(httpPort: Int): ConnectorBuilder = { server: Server -> ServerConnector(server).apply { port = httpPort } }
|
|
||||||
|
|||||||
@@ -7,8 +7,8 @@ import io.micronaut.context.annotation.Factory
|
|||||||
import io.micronaut.context.annotation.Primary
|
import io.micronaut.context.annotation.Primary
|
||||||
import org.http4k.core.RequestContexts
|
import org.http4k.core.RequestContexts
|
||||||
import org.http4k.lens.RequestContextKey
|
import org.http4k.lens.RequestContextKey
|
||||||
import javax.inject.Named
|
import jakarta.inject.Named
|
||||||
import javax.inject.Singleton
|
import jakarta.inject.Singleton
|
||||||
|
|
||||||
@Factory
|
@Factory
|
||||||
class AuthModule {
|
class AuthModule {
|
||||||
@@ -39,7 +39,7 @@ class AuthModule {
|
|||||||
simpleJwt = simpleJwt,
|
simpleJwt = simpleJwt,
|
||||||
lens = lens,
|
lens = lens,
|
||||||
source = JwtSource.Header,
|
source = JwtSource.Header,
|
||||||
redirect = false
|
redirect = false,
|
||||||
)
|
)
|
||||||
|
|
||||||
@Singleton
|
@Singleton
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import kotlinx.serialization.json.Json
|
|||||||
import kotlinx.serialization.modules.SerializersModule
|
import kotlinx.serialization.modules.SerializersModule
|
||||||
import java.time.LocalDateTime
|
import java.time.LocalDateTime
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import javax.inject.Singleton
|
import jakarta.inject.Singleton
|
||||||
|
|
||||||
@Factory
|
@Factory
|
||||||
class JsonModule {
|
class JsonModule {
|
||||||
|
|||||||
@@ -9,8 +9,8 @@ import io.micronaut.context.annotation.Factory
|
|||||||
import org.eclipse.jetty.server.ServerConnector
|
import org.eclipse.jetty.server.ServerConnector
|
||||||
import org.http4k.server.Http4kServer
|
import org.http4k.server.Http4kServer
|
||||||
import org.http4k.server.asServer
|
import org.http4k.server.asServer
|
||||||
import javax.inject.Named
|
import jakarta.inject.Named
|
||||||
import javax.inject.Singleton
|
import jakarta.inject.Singleton
|
||||||
import org.eclipse.jetty.server.Server as JettyServer
|
import org.eclipse.jetty.server.Server as JettyServer
|
||||||
import org.http4k.server.ServerConfig as Http4kServerConfig
|
import org.http4k.server.ServerConfig as Http4kServerConfig
|
||||||
|
|
||||||
|
|||||||
@@ -12,8 +12,8 @@ import org.http4k.routing.RoutingHttpHandler
|
|||||||
import org.http4k.routing.bind
|
import org.http4k.routing.bind
|
||||||
import org.http4k.routing.routes
|
import org.http4k.routing.routes
|
||||||
import java.util.function.Supplier
|
import java.util.function.Supplier
|
||||||
import javax.inject.Named
|
import jakarta.inject.Named
|
||||||
import javax.inject.Singleton
|
import jakarta.inject.Singleton
|
||||||
|
|
||||||
@Singleton
|
@Singleton
|
||||||
class ApiRoutes(
|
class ApiRoutes(
|
||||||
@@ -23,7 +23,6 @@ class ApiRoutes(
|
|||||||
@Named("required") private val authLens: RequiredAuthLens,
|
@Named("required") private val authLens: RequiredAuthLens,
|
||||||
) : Supplier<RoutingHttpHandler> {
|
) : Supplier<RoutingHttpHandler> {
|
||||||
override fun get(): RoutingHttpHandler {
|
override fun get(): RoutingHttpHandler {
|
||||||
|
|
||||||
infix fun PathMethod.to(action: ProtectedHandler) =
|
infix fun PathMethod.to(action: ProtectedHandler) =
|
||||||
this to { req: Request -> action(req, authLens(req)) }
|
this to { req: Request -> action(req, authLens(req)) }
|
||||||
|
|
||||||
@@ -38,9 +37,9 @@ class ApiRoutes(
|
|||||||
"/search" bind POST to ::search,
|
"/search" bind POST to ::search,
|
||||||
"/{uuid}" bind GET to ::note,
|
"/{uuid}" bind GET to ::note,
|
||||||
"/{uuid}" bind PUT to ::update,
|
"/{uuid}" bind PUT to ::update,
|
||||||
)
|
),
|
||||||
).withBasePath("/notes")
|
).withBasePath("/notes")
|
||||||
}
|
},
|
||||||
|
|
||||||
).withBasePath("/api")
|
).withBasePath("/api")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package be.simplenotes.app.routes
|
package be.simplenotes.app.routes
|
||||||
|
|
||||||
import be.simplenotes.app.controllers.BaseController
|
import be.simplenotes.app.controllers.BaseController
|
||||||
import be.simplenotes.app.controllers.HealthCheckController
|
|
||||||
import be.simplenotes.app.controllers.NoteController
|
import be.simplenotes.app.controllers.NoteController
|
||||||
import be.simplenotes.app.controllers.UserController
|
import be.simplenotes.app.controllers.UserController
|
||||||
import be.simplenotes.app.filters.ImmutableFilter
|
import be.simplenotes.app.filters.ImmutableFilter
|
||||||
@@ -14,12 +13,11 @@ import org.http4k.core.Request
|
|||||||
import org.http4k.core.then
|
import org.http4k.core.then
|
||||||
import org.http4k.routing.*
|
import org.http4k.routing.*
|
||||||
import java.util.function.Supplier
|
import java.util.function.Supplier
|
||||||
import javax.inject.Named
|
import jakarta.inject.Named
|
||||||
import javax.inject.Singleton
|
import jakarta.inject.Singleton
|
||||||
|
|
||||||
@Singleton
|
@Singleton
|
||||||
class BasicRoutes(
|
class BasicRoutes(
|
||||||
private val healthCheckController: HealthCheckController,
|
|
||||||
private val baseCtrl: BaseController,
|
private val baseCtrl: BaseController,
|
||||||
private val userCtrl: UserController,
|
private val userCtrl: UserController,
|
||||||
private val noteCtrl: NoteController,
|
private val noteCtrl: NoteController,
|
||||||
@@ -28,7 +26,6 @@ class BasicRoutes(
|
|||||||
) : Supplier<RoutingHttpHandler> {
|
) : Supplier<RoutingHttpHandler> {
|
||||||
|
|
||||||
override fun get(): RoutingHttpHandler {
|
override fun get(): RoutingHttpHandler {
|
||||||
|
|
||||||
infix fun PathMethod.to(action: PublicHandler) =
|
infix fun PathMethod.to(action: PublicHandler) =
|
||||||
this to { req: Request -> action(req, authLens(req)) }
|
this to { req: Request -> action(req, authLens(req)) }
|
||||||
|
|
||||||
@@ -36,8 +33,8 @@ class BasicRoutes(
|
|||||||
static(
|
static(
|
||||||
ResourceLoader.Classpath("/static"),
|
ResourceLoader.Classpath("/static"),
|
||||||
"woff2" to ContentType("font/woff2"),
|
"woff2" to ContentType("font/woff2"),
|
||||||
"webmanifest" to ContentType("application/manifest+json")
|
"webmanifest" to ContentType("application/manifest+json"),
|
||||||
)
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
return routes(
|
return routes(
|
||||||
@@ -50,11 +47,9 @@ class BasicRoutes(
|
|||||||
"/login" bind POST to userCtrl::login,
|
"/login" bind POST to userCtrl::login,
|
||||||
"/logout" bind POST to userCtrl::logout,
|
"/logout" bind POST to userCtrl::logout,
|
||||||
"/notes/public/{uuid}" bind GET to noteCtrl::public,
|
"/notes/public/{uuid}" bind GET to noteCtrl::public,
|
||||||
)
|
|
||||||
),
|
),
|
||||||
|
),
|
||||||
"/health" bind GET to healthCheckController::healthCheck,
|
staticHandler,
|
||||||
staticHandler
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,8 +12,8 @@ import org.http4k.routing.RoutingHttpHandler
|
|||||||
import org.http4k.routing.bind
|
import org.http4k.routing.bind
|
||||||
import org.http4k.routing.routes
|
import org.http4k.routing.routes
|
||||||
import java.util.function.Supplier
|
import java.util.function.Supplier
|
||||||
import javax.inject.Named
|
import jakarta.inject.Named
|
||||||
import javax.inject.Singleton
|
import jakarta.inject.Singleton
|
||||||
|
|
||||||
@Singleton
|
@Singleton
|
||||||
class NoteRoutes(
|
class NoteRoutes(
|
||||||
@@ -22,7 +22,6 @@ class NoteRoutes(
|
|||||||
@Named("required") private val authLens: RequiredAuthLens,
|
@Named("required") private val authLens: RequiredAuthLens,
|
||||||
) : Supplier<RoutingHttpHandler> {
|
) : Supplier<RoutingHttpHandler> {
|
||||||
override fun get(): RoutingHttpHandler {
|
override fun get(): RoutingHttpHandler {
|
||||||
|
|
||||||
infix fun PathMethod.to(action: ProtectedHandler) =
|
infix fun PathMethod.to(action: ProtectedHandler) =
|
||||||
this to { req: Request -> action(req, authLens(req)) }
|
this to { req: Request -> action(req, authLens(req)) }
|
||||||
|
|
||||||
@@ -40,7 +39,7 @@ class NoteRoutes(
|
|||||||
"/{uuid}/edit" bind POST to ::edit,
|
"/{uuid}/edit" bind POST to ::edit,
|
||||||
"/deleted/{uuid}" bind POST to ::deleted,
|
"/deleted/{uuid}" bind POST to ::deleted,
|
||||||
).withBasePath("/notes")
|
).withBasePath("/notes")
|
||||||
}
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import org.http4k.filter.ServerFilters.InitialiseRequestContext
|
|||||||
import org.http4k.routing.RoutingHttpHandler
|
import org.http4k.routing.RoutingHttpHandler
|
||||||
import org.http4k.routing.routes
|
import org.http4k.routing.routes
|
||||||
import java.util.function.Supplier
|
import java.util.function.Supplier
|
||||||
import javax.inject.Singleton
|
import jakarta.inject.Singleton
|
||||||
|
|
||||||
@Singleton
|
@Singleton
|
||||||
class Router(
|
class Router(
|
||||||
@@ -18,9 +18,8 @@ class Router(
|
|||||||
private val subRouters: List<Supplier<RoutingHttpHandler>>,
|
private val subRouters: List<Supplier<RoutingHttpHandler>>,
|
||||||
) {
|
) {
|
||||||
operator fun invoke(): RoutingHttpHandler {
|
operator fun invoke(): RoutingHttpHandler {
|
||||||
|
|
||||||
val routes = routes(
|
val routes = routes(
|
||||||
*subRouters.map { it.get() }.toTypedArray()
|
*subRouters.map { it.get() }.toTypedArray(),
|
||||||
)
|
)
|
||||||
|
|
||||||
return errorFilter
|
return errorFilter
|
||||||
|
|||||||
@@ -12,8 +12,8 @@ import org.http4k.routing.RoutingHttpHandler
|
|||||||
import org.http4k.routing.bind
|
import org.http4k.routing.bind
|
||||||
import org.http4k.routing.routes
|
import org.http4k.routing.routes
|
||||||
import java.util.function.Supplier
|
import java.util.function.Supplier
|
||||||
import javax.inject.Named
|
import jakarta.inject.Named
|
||||||
import javax.inject.Singleton
|
import jakarta.inject.Singleton
|
||||||
|
|
||||||
@Singleton
|
@Singleton
|
||||||
class SettingsRoutes(
|
class SettingsRoutes(
|
||||||
@@ -22,7 +22,6 @@ class SettingsRoutes(
|
|||||||
@Named("required") private val authLens: RequiredAuthLens,
|
@Named("required") private val authLens: RequiredAuthLens,
|
||||||
) : Supplier<RoutingHttpHandler> {
|
) : Supplier<RoutingHttpHandler> {
|
||||||
override fun get(): RoutingHttpHandler {
|
override fun get(): RoutingHttpHandler {
|
||||||
|
|
||||||
infix fun PathMethod.to(action: ProtectedHandler) =
|
infix fun PathMethod.to(action: ProtectedHandler) =
|
||||||
this to { req: Request -> action(req, authLens(req)) }
|
this to { req: Request -> action(req, authLens(req)) }
|
||||||
|
|
||||||
@@ -31,7 +30,8 @@ class SettingsRoutes(
|
|||||||
"/settings" bind GET to settingsController::settings,
|
"/settings" bind GET to settingsController::settings,
|
||||||
"/settings" bind POST to settingsController::settings,
|
"/settings" bind POST to settingsController::settings,
|
||||||
"/export" bind POST to settingsController::export,
|
"/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 {
|
override fun deserialize(decoder: Decoder): LocalDateTime {
|
||||||
TODO("Not implemented, isn't needed")
|
return LocalDateTime.parse(decoder.decodeString())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ 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
|
import jakarta.inject.Singleton
|
||||||
|
|
||||||
interface StaticFileResolver {
|
interface StaticFileResolver {
|
||||||
fun resolve(name: String): String?
|
fun resolve(name: String): String?
|
||||||
|
|||||||
@@ -58,8 +58,8 @@ internal class RequiredAuthFilterTest {
|
|||||||
},
|
},
|
||||||
"/protected" bind GET to requiredAuth.then { request: Request ->
|
"/protected" bind GET to requiredAuth.then { request: Request ->
|
||||||
Response(OK).body(requiredLens(request).toString())
|
Response(OK).body(requiredLens(request).toString())
|
||||||
}
|
},
|
||||||
)
|
),
|
||||||
)
|
)
|
||||||
// endregion
|
// endregion
|
||||||
|
|
||||||
@@ -106,7 +106,7 @@ internal class RequiredAuthFilterTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `it should allow a valid token"`() {
|
fun `it should allow a valid token`() {
|
||||||
val jwtPayload = LoggedInUser(1, "user")
|
val jwtPayload = LoggedInUser(1, "user")
|
||||||
val token = simpleJwt.sign(jwtPayload)
|
val token = simpleJwt.sign(jwtPayload)
|
||||||
val response = app(Request(GET, "/protected").cookie("Bearer", token))
|
val response = app(Request(GET, "/protected").cookie("Bearer", token))
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
plugins {
|
||||||
|
id("be.simplenotes.versions")
|
||||||
|
}
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
org.gradle.caching=true
|
|
||||||
org.gradle.parallel=true
|
|
||||||
@@ -2,19 +2,14 @@ plugins {
|
|||||||
`kotlin-dsl`
|
`kotlin-dsl`
|
||||||
}
|
}
|
||||||
|
|
||||||
kotlinDslPluginOptions {
|
|
||||||
experimentalWarning.set(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
repositories {
|
repositories {
|
||||||
gradlePluginPortal()
|
gradlePluginPortal()
|
||||||
maven { setUrl("https://kotlin.bintray.com/kotlinx") }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation("org.jetbrains.kotlin:kotlin-gradle-plugin:1.4.10")
|
implementation("org.jetbrains.kotlin:kotlin-gradle-plugin:1.8.21")
|
||||||
implementation("org.jetbrains.kotlin:kotlin-serialization:1.4.10")
|
implementation("org.jetbrains.kotlin:kotlin-serialization:1.8.21")
|
||||||
implementation("com.github.jengelman.gradle.plugins:shadow:6.1.0")
|
implementation("com.github.johnrengelman:shadow:8.1.1")
|
||||||
implementation("org.jlleitschuh.gradle:ktlint-gradle:9.4.1")
|
implementation("com.diffplug.spotless:spotless-plugin-gradle:6.13.0")
|
||||||
implementation("com.github.ben-manes:gradle-versions-plugin:0.28.0")
|
implementation("com.github.ben-manes:gradle-versions-plugin:0.46.0")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,44 @@
|
|||||||
|
package be.simplenotes
|
||||||
|
|
||||||
|
import org.gradle.api.Plugin
|
||||||
|
import org.gradle.api.Project
|
||||||
|
import org.gradle.kotlin.dsl.create
|
||||||
|
|
||||||
|
open class DockerPluginExtension {
|
||||||
|
var image: String? = null
|
||||||
|
var tag = "latest"
|
||||||
|
}
|
||||||
|
|
||||||
|
class DockerPlugin : Plugin<Project> {
|
||||||
|
override fun apply(project: Project) {
|
||||||
|
val extension = project.extensions.create<DockerPluginExtension>("docker")
|
||||||
|
|
||||||
|
project.task("dockerBuild") {
|
||||||
|
dependsOn("package")
|
||||||
|
|
||||||
|
group = "docker"
|
||||||
|
description = "Build a docker image"
|
||||||
|
|
||||||
|
doLast {
|
||||||
|
project.exec {
|
||||||
|
commandLine("docker", "build", "-t", "${extension.image}:${extension.tag}", ".")
|
||||||
|
workingDir(project.rootProject.projectDir)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
project.task("dockerPush") {
|
||||||
|
dependsOn("dockerBuild")
|
||||||
|
|
||||||
|
group = "docker"
|
||||||
|
description = "Push a docker image"
|
||||||
|
|
||||||
|
doLast {
|
||||||
|
project.exec {
|
||||||
|
commandLine("docker", "push", "${extension.image}:${extension.tag}")
|
||||||
|
workingDir(project.rootProject.projectDir)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,43 +3,11 @@
|
|||||||
package be.simplenotes
|
package be.simplenotes
|
||||||
|
|
||||||
object Libs {
|
object Libs {
|
||||||
const val arrowCoreData = "io.arrow-kt:arrow-core-data:0.11.0"
|
|
||||||
const val commonsCompress = "org.apache.commons:commons-compress:1.20"
|
|
||||||
const val flexmark = "com.vladsch.flexmark:flexmark:0.62.2"
|
|
||||||
const val flexmarkGfmTasklist = "com.vladsch.flexmark:flexmark-ext-gfm-tasklist:0.62.2"
|
|
||||||
const val flywayCore = "org.flywaydb:flyway-core:6.5.4"
|
|
||||||
const val h2 = "com.h2database:h2:1.4.200"
|
|
||||||
const val hikariCP = "com.zaxxer:HikariCP:3.4.3"
|
|
||||||
const val http4kCore = "org.http4k:http4k-core:3.268.0"
|
|
||||||
const val javaJwt = "com.auth0:java-jwt:3.10.3"
|
|
||||||
const val javaxServlet = "javax.servlet:javax.servlet-api:4.0.1"
|
|
||||||
const val jbcrypt = "org.mindrot:jbcrypt:0.4"
|
|
||||||
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 konform = "io.konform:konform-jvm:0.2.0"
|
|
||||||
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 ktormCore = "me.liuwj.ktorm:ktorm-core:3.0.0"
|
|
||||||
const val ktormMysql = "me.liuwj.ktorm:ktorm-support-mysql:3.0.0"
|
|
||||||
const val logbackClassic = "ch.qos.logback:logback-classic:1.2.3"
|
|
||||||
const val luceneAnalyzersCommon = "org.apache.lucene:lucene-analyzers-common:8.6.1"
|
|
||||||
const val luceneCore = "org.apache.lucene:lucene-core: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 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 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 slf4jApi = "org.slf4j:slf4j-api:1.7.25"
|
|
||||||
const val snakeyaml = "org.yaml:snakeyaml:1.26"
|
|
||||||
|
|
||||||
const val assertJ = "org.assertj:assertj-core:3.16.1"
|
object Micronaut {
|
||||||
const val hamkrest = "com.natpryce:hamkrest:1.7.0.3"
|
private const val version = "4.0.0-M2"
|
||||||
const val http4kTestingHamkrest = "org.http4k:http4k-testing-hamkrest:3.268.0"
|
const val inject = "io.micronaut:micronaut-inject:$version"
|
||||||
const val junit = "org.junit.jupiter:junit-jupiter:5.6.2"
|
const val processor = "io.micronaut:micronaut-inject-java:$version"
|
||||||
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"
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
package be.simplenotes
|
||||||
|
|
||||||
|
import org.gradle.api.Plugin
|
||||||
|
import org.gradle.api.Project
|
||||||
|
import org.gradle.api.artifacts.dsl.DependencyHandler
|
||||||
|
import org.jetbrains.kotlin.gradle.plugin.KaptExtension
|
||||||
|
|
||||||
|
class MicronautPlugin : Plugin<Project> {
|
||||||
|
override fun apply(target: Project) {
|
||||||
|
target.plugins.apply("org.jetbrains.kotlin.kapt")
|
||||||
|
target.extensions.configure<KaptExtension>("kapt") {
|
||||||
|
arguments {
|
||||||
|
arg("micronaut.processing.incremental", true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun DependencyHandler.micronaut() {
|
||||||
|
add("kapt", Libs.Micronaut.processor)
|
||||||
|
add("implementation", Libs.Micronaut.inject)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun DependencyHandler.micronautTest() {
|
||||||
|
add("kaptTest", Libs.Micronaut.processor)
|
||||||
|
add("testImplementation", Libs.Micronaut.inject)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun DependencyHandler.micronautFixtures() {
|
||||||
|
add("kaptTestFixtures", Libs.Micronaut.inject)
|
||||||
|
add("testFixturesImplementation", Libs.Micronaut.processor)
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
package be.simplenotes
|
||||||
|
|
||||||
|
import org.gradle.api.Plugin
|
||||||
|
import org.gradle.api.Project
|
||||||
|
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
|
||||||
|
|
||||||
|
class PostcssPlugin : Plugin<Project> {
|
||||||
|
override fun apply(project: Project) {
|
||||||
|
with(project.tasks) {
|
||||||
|
register<PostcssTask>("postcss") {
|
||||||
|
group = "postcss"
|
||||||
|
description = "generate postcss resources"
|
||||||
|
}
|
||||||
|
getByName("processResources").dependsOn("postcss")
|
||||||
|
}
|
||||||
|
|
||||||
|
val sourceSets = project.extensions.getByType<SourceSetContainer>()
|
||||||
|
val root = File("${project.buildDir}/generated-resources/css")
|
||||||
|
sourceSets["main"].resources.srcDir(root)
|
||||||
|
}
|
||||||
|
}
|
||||||
+2
-4
@@ -9,9 +9,7 @@ import java.lang.ProcessBuilder.Redirect.PIPE
|
|||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
import kotlin.concurrent.thread
|
import kotlin.concurrent.thread
|
||||||
|
|
||||||
open class CssTask : DefaultTask() {
|
open class PostcssTask : DefaultTask() {
|
||||||
|
|
||||||
private val root = project.parent!!.rootDir
|
|
||||||
|
|
||||||
private val viewsProject = project
|
private val viewsProject = project
|
||||||
.parent
|
.parent
|
||||||
@@ -54,7 +52,7 @@ open class CssTask : DefaultTask() {
|
|||||||
|
|
||||||
outputRootDir.deleteRecursively()
|
outputRootDir.deleteRecursively()
|
||||||
|
|
||||||
ProcessBuilder("yarn", "run", "postcss", "build", "$cssIndex", "--output", "$cssOutput")
|
ProcessBuilder("yarn", "run", "postcss", "$cssIndex", "--output", "$cssOutput")
|
||||||
.apply {
|
.apply {
|
||||||
environment().let {
|
environment().let {
|
||||||
it["MANIFEST"] = "$manifestOutput"
|
it["MANIFEST"] = "$manifestOutput"
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
package be.simplenotes
|
|
||||||
|
|
||||||
import org.gradle.kotlin.dsl.register
|
|
||||||
|
|
||||||
plugins {
|
|
||||||
java apply false
|
|
||||||
}
|
|
||||||
|
|
||||||
tasks.register<CssTask>("css")
|
|
||||||
|
|
||||||
sourceSets {
|
|
||||||
val main by getting
|
|
||||||
val root = file("$buildDir/generated-resources/css")
|
|
||||||
main.resources.srcDir(root)
|
|
||||||
}
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
package be.simplenotes
|
|
||||||
|
|
||||||
tasks.create("dockerBuild") {
|
|
||||||
dependsOn("package")
|
|
||||||
|
|
||||||
doLast {
|
|
||||||
exec {
|
|
||||||
commandLine("docker", "build", "-t", "hubv/simplenotes:latest", ".")
|
|
||||||
workingDir(rootProject.projectDir)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
tasks.create("dockerPush") {
|
|
||||||
dependsOn("dockerBuild")
|
|
||||||
|
|
||||||
doLast {
|
|
||||||
exec {
|
|
||||||
commandLine("docker", "push", "hubv/simplenotes:latest")
|
|
||||||
workingDir(rootProject.projectDir)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -11,6 +11,10 @@ tasks.withType<ShadowJar> {
|
|||||||
archiveAppendix.set("with-dependencies")
|
archiveAppendix.set("with-dependencies")
|
||||||
manifest.attributes["Main-Class"] = "be.simplenotes.app.SimpleNotesKt"
|
manifest.attributes["Main-Class"] = "be.simplenotes.app.SimpleNotesKt"
|
||||||
|
|
||||||
|
// johnrengelman/shadow#449
|
||||||
|
// we need this for lucene-core
|
||||||
|
manifest.attributes["Multi-Release"] = "true"
|
||||||
|
|
||||||
mergeServiceFiles()
|
mergeServiceFiles()
|
||||||
|
|
||||||
File(rootProject.projectDir, "buildSrc/src/main/resources/exclusions")
|
File(rootProject.projectDir, "buildSrc/src/main/resources/exclusions")
|
||||||
@@ -30,7 +34,6 @@ tasks.create("package") {
|
|||||||
tasks.getByName("build").dependsOn("package")
|
tasks.getByName("build").dependsOn("package")
|
||||||
|
|
||||||
dependsOn("shadowJar")
|
dependsOn("shadowJar")
|
||||||
dependsOn("css")
|
|
||||||
|
|
||||||
doLast {
|
doLast {
|
||||||
println("SimpleNotes Packaged !")
|
println("SimpleNotes Packaged !")
|
||||||
|
|||||||
@@ -4,6 +4,11 @@ plugins {
|
|||||||
id("be.simplenotes.java-convention")
|
id("be.simplenotes.java-convention")
|
||||||
id("be.simplenotes.kotlin-convention")
|
id("be.simplenotes.kotlin-convention")
|
||||||
id("be.simplenotes.junit-convention")
|
id("be.simplenotes.junit-convention")
|
||||||
id("org.jlleitschuh.gradle.ktlint")
|
id("com.diffplug.spotless")
|
||||||
id("com.github.ben-manes.versions")
|
}
|
||||||
|
|
||||||
|
spotless {
|
||||||
|
kotlin {
|
||||||
|
ktlint("0.48.0").setEditorConfigPath(project.rootProject.file(".editorconfig"))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,29 +1,28 @@
|
|||||||
package be.simplenotes
|
package be.simplenotes
|
||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
java
|
|
||||||
`java-library`
|
`java-library`
|
||||||
}
|
}
|
||||||
|
|
||||||
repositories {
|
repositories {
|
||||||
mavenLocal()
|
|
||||||
mavenCentral()
|
mavenCentral()
|
||||||
jcenter()
|
|
||||||
maven { url = uri("https://dl.bintray.com/arrow-kt/arrow-kt/") }
|
|
||||||
maven { url = uri("https://kotlin.bintray.com/kotlinx") }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
group = "be.simplenotes"
|
group = "be.simplenotes"
|
||||||
version = "1.0-SNAPSHOT"
|
version = "1.0-SNAPSHOT"
|
||||||
|
|
||||||
java {
|
java {
|
||||||
sourceCompatibility = JavaVersion.VERSION_14
|
toolchain {
|
||||||
targetCompatibility = JavaVersion.VERSION_14
|
languageVersion.set(JavaLanguageVersion.of(19))
|
||||||
|
vendor.set(JvmVendorSpec.ORACLE)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
tasks.withType<JavaCompile> {
|
tasks.withType<JavaCompile> {
|
||||||
options.encoding = "UTF-8"
|
options.encoding = "UTF-8"
|
||||||
}
|
}
|
||||||
|
|
||||||
sourceSets["main"].resources.srcDirs("resources")
|
sourceSets["main"].resources.setSrcDirs(listOf("resources"))
|
||||||
sourceSets["test"].resources.srcDirs("testresources")
|
sourceSets["main"].java.setSrcDirs(emptyList<String>())
|
||||||
|
sourceSets["test"].resources.setSrcDirs(listOf("testresources"))
|
||||||
|
sourceSets["test"].java.setSrcDirs(emptyList<String>())
|
||||||
|
|||||||
@@ -5,10 +5,11 @@ plugins {
|
|||||||
}
|
}
|
||||||
|
|
||||||
tasks.withType<Test> {
|
tasks.withType<Test> {
|
||||||
useJUnitPlatform()
|
useJUnitPlatform {
|
||||||
|
excludeTags("slow")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
sourceSets {
|
dependencies {
|
||||||
val test by getting
|
testRuntimeOnly(project(":junit-config"))
|
||||||
test.resources.srcDir("${rootProject.projectDir}/testresources/src/test/resources")
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
package be.simplenotes
|
package be.simplenotes
|
||||||
|
|
||||||
|
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
|
||||||
|
import org.jetbrains.kotlin.gradle.dsl.KotlinVersion
|
||||||
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
|
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
|
||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
@@ -8,21 +10,22 @@ plugins {
|
|||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation(kotlin("stdlib-jdk8"))
|
implementation(kotlin("stdlib-jdk8"))
|
||||||
implementation(platform("org.jetbrains.kotlin:kotlin-bom:1.4.10"))
|
implementation(platform(kotlin("bom")))
|
||||||
|
testImplementation(platform(kotlin("bom")))
|
||||||
}
|
}
|
||||||
|
|
||||||
tasks.withType<KotlinCompile> {
|
tasks.withType<KotlinCompile> {
|
||||||
kotlinOptions {
|
compilerOptions {
|
||||||
jvmTarget = "14"
|
jvmTarget.set(JvmTarget.JVM_19)
|
||||||
javaParameters = true
|
javaParameters.set(true)
|
||||||
freeCompilerArgs = listOf(
|
freeCompilerArgs.addAll(
|
||||||
"-Xinline-classes",
|
"-Xinline-classes",
|
||||||
"-Xno-param-assertions",
|
"-Xno-param-assertions",
|
||||||
"-Xno-call-assertions",
|
"-Xno-call-assertions",
|
||||||
"-Xno-receiver-assertions"
|
"-Xno-receiver-assertions",
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
kotlin.sourceSets["main"].kotlin.srcDirs("src")
|
kotlin.sourceSets["main"].kotlin.setSrcDirs(listOf("src"))
|
||||||
kotlin.sourceSets["test"].kotlin.srcDirs("test")
|
kotlin.sourceSets["test"].kotlin.setSrcDirs(listOf("test"))
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
package be.simplenotes
|
||||||
|
|
||||||
|
import com.github.benmanes.gradle.versions.updates.DependencyUpdatesTask
|
||||||
|
|
||||||
|
plugins {
|
||||||
|
id("com.github.ben-manes.versions")
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks.named<DependencyUpdatesTask>("dependencyUpdates").configure {
|
||||||
|
resolutionStrategy {
|
||||||
|
componentSelection {
|
||||||
|
all {
|
||||||
|
if ("alpha|beta|rc".toRegex().containsMatchIn(candidate.version.lowercase())) reject("Non stable version")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
implementation-class=be.simplenotes.DockerPlugin
|
||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
implementation-class=be.simplenotes.MicronautPlugin
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
implementation-class=be.simplenotes.PostcssPlugin
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
META-INF/maven/**
|
META-INF/maven/**
|
||||||
META-INF/proguard/**
|
META-INF/proguard/**
|
||||||
|
META-INF/com.android.tools/**
|
||||||
META-INF/*.kotlin_module
|
META-INF/*.kotlin_module
|
||||||
META-INF/DEPENDENCIES*
|
META-INF/DEPENDENCIES*
|
||||||
META-INF/NOTICE*
|
META-INF/NOTICE*
|
||||||
@@ -7,6 +8,7 @@ META-INF/LICENSE*
|
|||||||
LICENSE*
|
LICENSE*
|
||||||
META-INF/README*
|
META-INF/README*
|
||||||
META-INF/native-image/**
|
META-INF/native-image/**
|
||||||
|
**/module-info.**
|
||||||
|
|
||||||
# Jetty
|
# Jetty
|
||||||
about.html
|
about.html
|
||||||
|
|||||||
@@ -1,11 +1,14 @@
|
|||||||
import be.simplenotes.Libs
|
import be.simplenotes.micronaut
|
||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
id("be.simplenotes.base")
|
id("be.simplenotes.base")
|
||||||
kotlin("kapt")
|
id("be.simplenotes.micronaut")
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation(Libs.micronaut)
|
micronaut()
|
||||||
kapt(Libs.micronautProcessor)
|
runtimeOnly(libs.yaml)
|
||||||
|
|
||||||
|
testImplementation(libs.bundles.test)
|
||||||
|
testRuntimeOnly(libs.slf4j.logback)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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:
|
jwt:
|
||||||
secret: 'PliLvfk7l4WF+cZJk66LR5Mpnh+ocbvJ2wfUCK2UCms='
|
secret: 'PliLvfk7l4WF+cZJk66LR5Mpnh+ocbvJ2wfUCK2UCms='
|
||||||
validity: 24
|
validity: 24
|
||||||
@@ -14,3 +6,5 @@ jwt:
|
|||||||
server:
|
server:
|
||||||
host: localhost
|
host: localhost
|
||||||
port: 8080
|
port: 8080
|
||||||
|
|
||||||
|
data-dir: ./data
|
||||||
|
|||||||
@@ -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,
|
|
||||||
)
|
|
||||||
@@ -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")
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
package be.simplenotes.config
|
||||||
|
|
||||||
|
import io.micronaut.context.ApplicationContext
|
||||||
|
import org.assertj.core.api.Assertions.assertThat
|
||||||
|
import org.junit.jupiter.api.Test
|
||||||
|
|
||||||
|
class ConfigTest {
|
||||||
|
private val ctx = ApplicationContext.run()
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `check application yaml is on the classpath`() {
|
||||||
|
val yaml = javaClass.getResource("/application.yaml")
|
||||||
|
assertThat(yaml).`as`("The application.yaml resource").isNotNull
|
||||||
|
assertThat(yaml.readText()).`as`("The config content").isNotBlank
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `check config properties`() {
|
||||||
|
assertThat(ctx.getProperty("jwt.validity", Int::class.java))
|
||||||
|
.`as`("Jwt.validity")
|
||||||
|
.isPresent
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `load jwt config`() {
|
||||||
|
val jwtConfig = ctx.getBean(JwtConfig::class.java)
|
||||||
|
assertThat(jwtConfig).isNotNull
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
<configuration>
|
<configuration>
|
||||||
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
|
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
|
||||||
<withJansi>true</withJansi>
|
|
||||||
<encoder>
|
<encoder>
|
||||||
<pattern>%cyan(%d{YYYY-MM-dd HH:mm:ss.SSS}) [%thread] %highlight(%-5level) %green(%logger{36}) - %msg%n
|
<pattern>%cyan(%d{YYYY-MM-dd HH:mm:ss.SSS}) [%thread] %highlight(%-5level) %green(%logger{36}) - %msg%n
|
||||||
</pattern>
|
</pattern>
|
||||||
@@ -9,7 +8,6 @@
|
|||||||
<root level="DEBUG">
|
<root level="DEBUG">
|
||||||
<appender-ref ref="STDOUT"/>
|
<appender-ref ref="STDOUT"/>
|
||||||
</root>
|
</root>
|
||||||
<logger name="me.liuwj.ktorm.database" level="DEBUG"/>
|
<logger name="io.micronaut" level="TRACE"/>
|
||||||
<logger name="com.zaxxer.hikari" level="INFO"/>
|
<logger name="io.micronaut.context.lifecycle" level="TRACE"/>
|
||||||
<logger name="org.flywaydb.core" level="DEBUG"/>
|
|
||||||
</configuration>
|
</configuration>
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
plugins {
|
||||||
|
id("be.simplenotes.java-convention")
|
||||||
|
id("be.simplenotes.postcss")
|
||||||
|
}
|
||||||
+6
-7
@@ -5,12 +5,11 @@
|
|||||||
"//": "`gradle css`"
|
"//": "`gradle css`"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"autoprefixer": "^9.8.6",
|
"autoprefixer": "^10.4.14",
|
||||||
"cssnano": "^4.1.10",
|
"cssnano": "^6.0.1",
|
||||||
"postcss-cli": "^7.1.1",
|
"postcss-cli": "^10.1.0",
|
||||||
"postcss-hash": "^2.0.0",
|
"postcss-hash": "^3.0.0",
|
||||||
"postcss-import": "^12.0.1",
|
"postcss-import": "^15.1.0",
|
||||||
"postcss-nested": "^4.2.3",
|
"tailwindcss": "^3.3.2"
|
||||||
"tailwindcss": "^1.5.1"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
plugins: [
|
plugins: [
|
||||||
require('postcss-import'),
|
require('postcss-import'),
|
||||||
require('postcss-nested'),
|
require('tailwindcss/nesting'),
|
||||||
require('tailwindcss'),
|
require('tailwindcss'),
|
||||||
require('autoprefixer'),
|
require('autoprefixer'),
|
||||||
require('cssnano')({
|
require('cssnano')({
|
||||||
|
|||||||
+1
-1
@@ -2,7 +2,7 @@
|
|||||||
@apply font-semibold py-2 px-4 rounded;
|
@apply font-semibold py-2 px-4 rounded;
|
||||||
|
|
||||||
&:focus {
|
&:focus {
|
||||||
@apply outline-none shadow-outline;
|
@apply outline-none ring;
|
||||||
}
|
}
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
#note {
|
#note {
|
||||||
a {
|
a {
|
||||||
@apply text-blue-700 underline;
|
@apply text-blue-500 underline;
|
||||||
}
|
}
|
||||||
|
|
||||||
p {
|
p {
|
||||||
|
|||||||
+1
-1
@@ -2,7 +2,7 @@
|
|||||||
@apply italic font-semibold text-sm bg-teal-500 text-gray-900 rounded-full py-1 px-2 align-middle;
|
@apply italic font-semibold text-sm bg-teal-500 text-gray-900 rounded-full py-1 px-2 align-middle;
|
||||||
|
|
||||||
&:focus {
|
&:focus {
|
||||||
@apply outline-none shadow-outline bg-teal-800 text-white;
|
@apply outline-none ring bg-teal-800 text-white;
|
||||||
}
|
}
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
purge: {
|
|
||||||
content: [
|
content: [
|
||||||
process.env.PURGE
|
process.env.PURGE
|
||||||
]
|
],
|
||||||
},
|
|
||||||
theme: {
|
theme: {
|
||||||
fontFamily: {
|
fontFamily: {
|
||||||
'sans': [
|
'sans': [
|
||||||
|
|||||||
+822
-1433
File diff suppressed because it is too large
Load Diff
+4
-41
@@ -1,56 +1,19 @@
|
|||||||
version: '2.2'
|
version: '2.2'
|
||||||
|
|
||||||
services:
|
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:
|
simplenotes:
|
||||||
image: hubv/simplenotes
|
image: hubv/simplenotes
|
||||||
container_name: simplenotes
|
container_name: simplenotes
|
||||||
env_file:
|
env_file:
|
||||||
- .env
|
- .env
|
||||||
environment:
|
environment:
|
||||||
|
- PUID=1000
|
||||||
|
- PGID=1000
|
||||||
- TZ=Europe/Brussels
|
- 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:
|
# .env:
|
||||||
# - JWT_SECRET
|
# - JWT_SECRET
|
||||||
# - DB_PASSWORD
|
|
||||||
ports:
|
ports:
|
||||||
- 127.0.0.1:8080:8080
|
- 127.0.0.1:8080:8080
|
||||||
healthcheck:
|
volumes:
|
||||||
test: "curl --fail -s http://localhost:8080/health"
|
- ./simplenotes-data:/app/data
|
||||||
interval: 5s
|
|
||||||
timeout: 1s
|
|
||||||
start_period: 2s
|
|
||||||
retries: 3
|
|
||||||
depends_on:
|
|
||||||
db:
|
|
||||||
condition: service_healthy
|
|
||||||
|
|
||||||
|
|
||||||
volumes:
|
|
||||||
notes-db-volume:
|
|
||||||
|
|||||||
+17
-20
@@ -1,32 +1,29 @@
|
|||||||
import be.simplenotes.Libs
|
import be.simplenotes.micronaut
|
||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
id("be.simplenotes.base")
|
id("be.simplenotes.base")
|
||||||
id("be.simplenotes.kotlinx-serialization")
|
id("be.simplenotes.kotlinx-serialization")
|
||||||
kotlin("kapt")
|
id("be.simplenotes.micronaut")
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation(project(":config"))
|
api(project(":config"))
|
||||||
implementation(project(":types"))
|
api(project(":types"))
|
||||||
implementation(project(":persistance"))
|
implementation(project(":persistence"))
|
||||||
implementation(project(":search"))
|
implementation(project(":search"))
|
||||||
|
|
||||||
implementation(Libs.micronaut)
|
api(libs.arrow.core)
|
||||||
kapt(Libs.micronautProcessor)
|
api(libs.konform)
|
||||||
|
|
||||||
implementation(Libs.kotlinxSerializationJson)
|
micronaut()
|
||||||
implementation(Libs.arrowCoreData)
|
|
||||||
implementation(Libs.konform)
|
|
||||||
implementation(Libs.jbcrypt)
|
|
||||||
implementation(Libs.javaJwt)
|
|
||||||
implementation(Libs.flexmark)
|
|
||||||
implementation(Libs.flexmarkGfmTasklist)
|
|
||||||
implementation(Libs.snakeyaml)
|
|
||||||
implementation(Libs.owaspHtmlSanitizer)
|
|
||||||
implementation(Libs.commonsCompress)
|
|
||||||
|
|
||||||
testImplementation(Libs.hamkrest)
|
implementation(libs.kotlinx.serialization.json)
|
||||||
testImplementation(Libs.junit)
|
implementation(libs.bcrypt)
|
||||||
testImplementation(Libs.mockk)
|
implementation(libs.jwt)
|
||||||
|
implementation(libs.bundles.flexmark)
|
||||||
|
implementation(libs.yaml)
|
||||||
|
implementation(libs.owasp.html.sanitizer)
|
||||||
|
implementation(libs.commonsCompress)
|
||||||
|
|
||||||
|
testImplementation(libs.bundles.test)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
package be.simplenotes.domain.usecases.export
|
package be.simplenotes.domain
|
||||||
|
|
||||||
import be.simplenotes.persistance.repositories.NoteRepository
|
import be.simplenotes.persistence.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
|
||||||
@@ -10,14 +9,19 @@ 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
|
import jakarta.inject.Singleton
|
||||||
|
|
||||||
|
interface ExportService {
|
||||||
|
fun exportAsJson(userId: Int): String
|
||||||
|
fun exportAsZip(userId: Int): InputStream
|
||||||
|
}
|
||||||
|
|
||||||
@Primary
|
|
||||||
@Singleton
|
@Singleton
|
||||||
internal class ExportUseCaseImpl(
|
internal class ExportServiceImpl(
|
||||||
private val noteRepository: NoteRepository,
|
private val noteRepository: NoteRepository,
|
||||||
private val json: Json,
|
private val json: Json,
|
||||||
) : ExportUseCase {
|
) : ExportService {
|
||||||
|
|
||||||
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)
|
||||||
@@ -38,7 +42,7 @@ internal class ExportUseCaseImpl(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class ZipOutput : AutoCloseable {
|
private class ZipOutput : AutoCloseable {
|
||||||
val outputStream = ByteArrayOutputStream()
|
val outputStream = ByteArrayOutputStream()
|
||||||
private val zipOutputStream = ZipArchiveOutputStream(outputStream)
|
private val zipOutputStream = ZipArchiveOutputStream(outputStream)
|
||||||
|
|
||||||
@@ -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 +0,0 @@
|
|||||||
package be.simplenotes.domain
|
|
||||||
@@ -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.security.HtmlSanitizer
|
||||||
import be.simplenotes.domain.usecases.markdown.MarkdownConverter
|
import be.simplenotes.domain.utils.parseSearchTerms
|
||||||
import be.simplenotes.domain.usecases.markdown.MarkdownParsingError
|
import be.simplenotes.persistence.repositories.NoteRepository
|
||||||
import be.simplenotes.domain.usecases.search.parseSearchTerms
|
import be.simplenotes.persistence.repositories.UserRepository
|
||||||
import be.simplenotes.persistance.repositories.NoteRepository
|
|
||||||
import be.simplenotes.persistance.repositories.UserRepository
|
|
||||||
import be.simplenotes.persistance.transactions.TransactionService
|
|
||||||
import be.simplenotes.search.NoteSearcher
|
import be.simplenotes.search.NoteSearcher
|
||||||
import be.simplenotes.types.LoggedInUser
|
import be.simplenotes.types.LoggedInUser
|
||||||
import be.simplenotes.types.Note
|
import be.simplenotes.types.Note
|
||||||
import be.simplenotes.types.PersistedNote
|
|
||||||
import be.simplenotes.types.PersistedNoteMetadata
|
import be.simplenotes.types.PersistedNoteMetadata
|
||||||
|
import jakarta.annotation.PostConstruct
|
||||||
|
import jakarta.annotation.PreDestroy
|
||||||
|
import jakarta.inject.Singleton
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import javax.annotation.PostConstruct
|
|
||||||
import javax.annotation.PreDestroy
|
|
||||||
import javax.inject.Singleton
|
|
||||||
|
|
||||||
@Singleton
|
@Singleton
|
||||||
class NoteService(
|
class NoteService(
|
||||||
private val markdownConverter: MarkdownConverter,
|
private val markdownService: MarkdownService,
|
||||||
private val noteRepository: NoteRepository,
|
private val noteRepository: NoteRepository,
|
||||||
private val userRepository: UserRepository,
|
private val userRepository: UserRepository,
|
||||||
private val searcher: NoteSearcher,
|
private val searcher: NoteSearcher,
|
||||||
private val htmlSanitizer: HtmlSanitizer,
|
private val htmlSanitizer: HtmlSanitizer,
|
||||||
private val transaction: TransactionService,
|
|
||||||
) {
|
) {
|
||||||
|
|
||||||
fun create(user: LoggedInUser, markdownText: String) = transaction.use {
|
fun create(user: LoggedInUser, markdownText: String) = either {
|
||||||
either.eager<MarkdownParsingError, PersistedNote> {
|
markdownService.renderDocument(markdownText)
|
||||||
val persistedNote = !markdownConverter.renderDocument(markdownText)
|
|
||||||
.map { it.copy(html = htmlSanitizer.sanitize(user, it.html)) }
|
.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) }
|
.map { noteRepository.create(user.userId, it) }
|
||||||
|
.bind()
|
||||||
searcher.indexNote(user.userId, persistedNote)
|
.also { searcher.indexNote(user.userId, it) }
|
||||||
persistedNote
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun update(
|
fun update(user: LoggedInUser, uuid: UUID, markdownText: String) =
|
||||||
user: LoggedInUser,
|
either {
|
||||||
uuid: UUID,
|
markdownService.renderDocument(markdownText)
|
||||||
markdownText: String,
|
|
||||||
) = transaction.use {
|
|
||||||
either.eager<MarkdownParsingError, PersistedNote?> {
|
|
||||||
val persistedNote = !markdownConverter.renderDocument(markdownText)
|
|
||||||
.map { it.copy(html = htmlSanitizer.sanitize(user, it.html)) }
|
.map { it.copy(html = htmlSanitizer.sanitize(user, it.html)) }
|
||||||
.map { Note(it.metadata, markdown = markdownText, html = it.html) }
|
.map {
|
||||||
.map { noteRepository.update(user.userId, uuid, it) }
|
Note(
|
||||||
|
title = it.metadata.title,
|
||||||
persistedNote?.let { searcher.updateIndex(user.userId, it) }
|
tags = it.metadata.tags,
|
||||||
persistedNote
|
markdown = markdownText,
|
||||||
|
html = it.html,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
.map { noteRepository.update(user.userId, uuid, it) }
|
||||||
|
.bind()
|
||||||
|
?.also { searcher.updateIndex(user.userId, it) }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun paginatedNotes(
|
fun paginatedNotes(
|
||||||
@@ -72,22 +65,22 @@ class NoteService(
|
|||||||
|
|
||||||
fun find(userId: Int, uuid: UUID) = noteRepository.find(userId, uuid)
|
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)
|
val res = noteRepository.delete(userId, uuid, permanent = false)
|
||||||
if (res) searcher.deleteIndex(userId, uuid)
|
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)
|
val res = noteRepository.restore(userId, uuid)
|
||||||
if (res) find(userId, uuid)?.let { note -> searcher.indexNote(userId, note) }
|
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)
|
val res = noteRepository.delete(userId, uuid, permanent = true)
|
||||||
if (res) searcher.deleteIndex(userId, uuid)
|
if (res) searcher.deleteIndex(userId, uuid)
|
||||||
res
|
return res
|
||||||
}
|
}
|
||||||
|
|
||||||
fun countDeleted(userId: Int) = noteRepository.count(userId, deleted = true)
|
fun countDeleted(userId: Int) = noteRepository.count(userId, deleted = true)
|
||||||
@@ -107,8 +100,8 @@ class NoteService(
|
|||||||
@PreDestroy
|
@PreDestroy
|
||||||
fun dropAllIndexes() = searcher.dropAll()
|
fun dropAllIndexes() = searcher.dropAll()
|
||||||
|
|
||||||
fun makePublic(userId: Int, uuid: UUID) = transaction.use { noteRepository.makePublic(userId, uuid) }
|
fun makePublic(userId: Int, uuid: UUID) = noteRepository.makePublic(userId, uuid)
|
||||||
fun makePrivate(userId: Int, uuid: UUID) = transaction.use { noteRepository.makePrivate(userId, uuid) }
|
fun makePrivate(userId: Int, uuid: UUID) = noteRepository.makePrivate(userId, uuid)
|
||||||
fun findPublic(uuid: UUID) = noteRepository.findPublic(uuid)
|
fun findPublic(uuid: UUID) = noteRepository.findPublic(uuid)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -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?)
|
||||||
+4
-4
@@ -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.ext.gfm.tasklist.TaskListExtension
|
||||||
import com.vladsch.flexmark.html.HtmlRenderer
|
import com.vladsch.flexmark.html.HtmlRenderer
|
||||||
import com.vladsch.flexmark.parser.Parser
|
import com.vladsch.flexmark.parser.Parser
|
||||||
import com.vladsch.flexmark.util.data.MutableDataSet
|
import com.vladsch.flexmark.util.data.MutableDataSet
|
||||||
import io.micronaut.context.annotation.Factory
|
import io.micronaut.context.annotation.Factory
|
||||||
import javax.inject.Singleton
|
import jakarta.inject.Singleton
|
||||||
|
|
||||||
@Factory
|
@Factory
|
||||||
class FlexmarkFactory {
|
class FlexmarkFactory {
|
||||||
@@ -23,11 +23,11 @@ class FlexmarkFactory {
|
|||||||
set(TaskListExtension.LOOSE_ITEM_CLASS, "")
|
set(TaskListExtension.LOOSE_ITEM_CLASS, "")
|
||||||
set(
|
set(
|
||||||
TaskListExtension.ITEM_DONE_MARKER,
|
TaskListExtension.ITEM_DONE_MARKER,
|
||||||
"""<input type="checkbox" checked="checked" disabled="disabled" readonly="readonly" /> """
|
"""<input type="checkbox" checked="checked" disabled="disabled" readonly="readonly" /> """,
|
||||||
)
|
)
|
||||||
set(
|
set(
|
||||||
TaskListExtension.ITEM_NOT_DONE_MARKER,
|
TaskListExtension.ITEM_NOT_DONE_MARKER,
|
||||||
"""<input type="checkbox" disabled="disabled" readonly="readonly" /> """
|
"""<input type="checkbox" disabled="disabled" readonly="readonly" /> """,
|
||||||
)
|
)
|
||||||
set(HtmlRenderer.SOFT_BREAK, "<br>")
|
set(HtmlRenderer.SOFT_BREAK, "<br>")
|
||||||
}
|
}
|
||||||
@@ -4,7 +4,7 @@ import be.simplenotes.types.LoggedInUser
|
|||||||
import org.owasp.html.HtmlChangeListener
|
import org.owasp.html.HtmlChangeListener
|
||||||
import org.owasp.html.HtmlPolicyBuilder
|
import org.owasp.html.HtmlPolicyBuilder
|
||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
import javax.inject.Singleton
|
import jakarta.inject.Singleton
|
||||||
|
|
||||||
@Singleton
|
@Singleton
|
||||||
class HtmlSanitizer {
|
class HtmlSanitizer {
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ package be.simplenotes.domain.security
|
|||||||
import be.simplenotes.types.LoggedInUser
|
import be.simplenotes.types.LoggedInUser
|
||||||
import com.auth0.jwt.JWTCreator
|
import com.auth0.jwt.JWTCreator
|
||||||
import com.auth0.jwt.interfaces.DecodedJWT
|
import com.auth0.jwt.interfaces.DecodedJWT
|
||||||
import javax.inject.Singleton
|
import jakarta.inject.Singleton
|
||||||
|
|
||||||
interface JwtMapper<T> {
|
interface JwtMapper<T> {
|
||||||
fun extract(decodedJWT: DecodedJWT): T?
|
fun extract(decodedJWT: DecodedJWT): T?
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
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 jakarta.inject.Inject
|
||||||
import javax.inject.Singleton
|
import jakarta.inject.Singleton
|
||||||
|
|
||||||
internal interface PasswordHash {
|
internal interface PasswordHash {
|
||||||
fun crypt(password: String): String
|
fun crypt(password: String): String
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import com.auth0.jwt.algorithms.Algorithm
|
|||||||
import com.auth0.jwt.exceptions.JWTVerificationException
|
import com.auth0.jwt.exceptions.JWTVerificationException
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
import javax.inject.Singleton
|
import jakarta.inject.Singleton
|
||||||
|
|
||||||
@Singleton
|
@Singleton
|
||||||
class SimpleJwt<T>(jwtConfig: JwtConfig, private val mapper: JwtMapper<T>) {
|
class SimpleJwt<T>(jwtConfig: JwtConfig, private val mapper: JwtMapper<T>) {
|
||||||
|
|||||||
@@ -1,13 +0,0 @@
|
|||||||
package be.simplenotes.domain.usecases
|
|
||||||
|
|
||||||
import be.simplenotes.persistance.DbHealthCheck
|
|
||||||
import javax.inject.Singleton
|
|
||||||
|
|
||||||
interface HealthCheckService {
|
|
||||||
fun isOk(): Boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
@Singleton
|
|
||||||
class HealthCheckServiceImpl(private val dbHealthCheck: DbHealthCheck) : HealthCheckService {
|
|
||||||
override fun isOk() = dbHealthCheck.isOk()
|
|
||||||
}
|
|
||||||
@@ -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.persistance.repositories.UserRepository
|
|
||||||
import be.simplenotes.persistance.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.persistance.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.persistance.repositories.UserRepository
|
|
||||||
import be.simplenotes.persistance.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>
|
|
||||||
}
|
|
||||||
@@ -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,
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
package be.simplenotes.domain.validation
|
package be.simplenotes.domain.validation
|
||||||
|
|
||||||
import be.simplenotes.domain.usecases.markdown.ValidationError
|
import be.simplenotes.domain.MarkdownParsingError
|
||||||
import be.simplenotes.types.NoteMetadata
|
import be.simplenotes.types.NoteMetadata
|
||||||
import io.konform.validation.Validation
|
import io.konform.validation.Validation
|
||||||
import io.konform.validation.jsonschema.maxItems
|
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
|
val errors = metaValidator.validate(meta).errors
|
||||||
return if (errors.isEmpty()) null
|
return if (errors.isEmpty()) null
|
||||||
else return ValidationError(errors)
|
else return MarkdownParsingError.ValidationError(errors)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,8 @@
|
|||||||
package be.simplenotes.domain.validation
|
package be.simplenotes.domain.validation
|
||||||
|
|
||||||
import arrow.core.Either
|
import arrow.core.raise.either
|
||||||
import arrow.core.left
|
import arrow.core.raise.ensure
|
||||||
import arrow.core.right
|
import be.simplenotes.domain.*
|
||||||
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 be.simplenotes.types.User
|
import be.simplenotes.types.User
|
||||||
import io.konform.validation.Validation
|
import io.konform.validation.Validation
|
||||||
import io.konform.validation.jsonschema.maxLength
|
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
|
val errors = loginValidator.validate(form).errors
|
||||||
return if (errors.isEmpty()) User(form.username!!, form.password!!).right()
|
ensure(errors.isEmpty()) { LoginError.InvalidLoginForm(errors) }
|
||||||
else return InvalidLoginForm(errors).left()
|
User(form.username!!, form.password!!)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun validateRegister(form: RegisterForm): Either<InvalidRegisterForm, User> {
|
fun validateRegister(form: RegisterForm) = either {
|
||||||
val errors = loginValidator.validate(form).errors
|
val errors = loginValidator.validate(form).errors
|
||||||
return if (errors.isEmpty()) User(form.username!!, form.password!!).right()
|
ensure(errors.isEmpty()) { RegisterError.InvalidRegisterForm(errors) }
|
||||||
else return InvalidRegisterForm(errors).left()
|
User(form.username!!, form.password!!)
|
||||||
}
|
}
|
||||||
|
|
||||||
private val deleteValidator = Validation<DeleteForm> {
|
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
|
val errors = deleteValidator.validate(form).errors
|
||||||
return if (errors.isEmpty()) User(form.username!!, form.password!!).right()
|
ensure(errors.isEmpty()) { DeleteError.InvalidForm(errors) }
|
||||||
else return DeleteError.InvalidForm(errors).left()
|
User(form.username!!, form.password!!)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
package be.simplenotes.domain
|
|
||||||
@@ -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
|
package be.simplenotes.domain.security
|
||||||
|
|
||||||
import be.simplenotes.config.JwtConfig
|
import be.simplenotes.config.JwtConfig
|
||||||
import be.simplenotes.domain.usecases.users.login.Token
|
import be.simplenotes.domain.Token
|
||||||
import be.simplenotes.types.LoggedInUser
|
import be.simplenotes.types.LoggedInUser
|
||||||
import com.auth0.jwt.JWT
|
import com.auth0.jwt.JWT
|
||||||
import com.auth0.jwt.algorithms.Algorithm
|
import com.auth0.jwt.algorithms.Algorithm
|
||||||
@@ -33,7 +33,7 @@ internal class LoggedInUserExtractorTest {
|
|||||||
createToken(),
|
createToken(),
|
||||||
createToken(username = "user", id = 1, secret = "not the correct secret"),
|
createToken(username = "user", id = 1, secret = "not the correct secret"),
|
||||||
createToken(username = "user", id = 1) + "\"efesfsef",
|
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")
|
@ParameterizedTest(name = "[{index}] token `{0}` should be invalid")
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ fun isRight() = object : Matcher<Either<*, *>> {
|
|||||||
override fun invoke(actual: Either<*, *>) = when (actual) {
|
override fun invoke(actual: Either<*, *>) = when (actual) {
|
||||||
is Either.Right -> MatchResult.Match
|
is Either.Right -> MatchResult.Match
|
||||||
is Either.Left -> {
|
is Either.Left -> {
|
||||||
val valueA = actual.a
|
val valueA = actual.value
|
||||||
MatchResult.Mismatch("is Either.Left<${if (valueA == null) "Null" else valueA::class.simpleName}>")
|
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) {
|
override fun invoke(actual: Either<*, *>) = when (actual) {
|
||||||
is Either.Right -> MatchResult.Mismatch("was Either.Right<>")
|
is Either.Right -> MatchResult.Mismatch("was Either.Right<>")
|
||||||
is Either.Left -> {
|
is Either.Left -> {
|
||||||
val valueA = actual.a
|
val valueA = actual.value
|
||||||
if (valueA is A) MatchResult.Match
|
if (valueA is A) MatchResult.Match
|
||||||
else MatchResult.Mismatch("was Left<${if (valueA == null) "Null" else valueA::class.simpleName}>")
|
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.persistance.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.persistance.repositories.UserRepository
|
|
||||||
import be.simplenotes.persistance.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)) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+10
-2
@@ -1,4 +1,4 @@
|
|||||||
package be.simplenotes.domain.usecases.search
|
package be.simplenotes.domain.utils
|
||||||
|
|
||||||
import be.simplenotes.search.SearchTerms
|
import be.simplenotes.search.SearchTerms
|
||||||
import com.natpryce.hamkrest.assertion.assertThat
|
import com.natpryce.hamkrest.assertion.assertThat
|
||||||
@@ -33,7 +33,15 @@ internal class SearchTermsParserKtTest {
|
|||||||
"tag:'example abc' title:'other with words' this is the end ",
|
"tag:'example abc' title:'other with words' this is the end ",
|
||||||
title = "other with words",
|
title = "other with words",
|
||||||
tag = "example abc",
|
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
|
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.isLeftOfType
|
||||||
import be.simplenotes.domain.testutils.isRight
|
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 com.natpryce.hamkrest.assertion.assertThat
|
||||||
import org.junit.jupiter.api.Nested
|
import org.junit.jupiter.api.Nested
|
||||||
import org.junit.jupiter.params.ParameterizedTest
|
import org.junit.jupiter.params.ParameterizedTest
|
||||||
@@ -22,19 +22,19 @@ internal class UserValidationsTest {
|
|||||||
LoginForm(username = "", password = ""),
|
LoginForm(username = "", password = ""),
|
||||||
LoginForm(username = "a", password = "aaaa"),
|
LoginForm(username = "a", password = "aaaa"),
|
||||||
LoginForm(username = "a".repeat(51), password = "a".repeat(8)),
|
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
|
@ParameterizedTest
|
||||||
@MethodSource("invalidLoginForms")
|
@MethodSource("invalidLoginForms")
|
||||||
fun `validate invalid logins`(form: LoginForm) {
|
fun `validate invalid logins`(form: LoginForm) {
|
||||||
assertThat(UserValidations.validateLogin(form), isLeftOfType<InvalidLoginForm>())
|
assertThat(UserValidations.validateLogin(form), isLeftOfType<LoginError.InvalidLoginForm>())
|
||||||
}
|
}
|
||||||
|
|
||||||
@Suppress("Unused")
|
@Suppress("Unused")
|
||||||
fun validLoginForms(): Stream<LoginForm> = Stream.of(
|
fun validLoginForms(): Stream<LoginForm> = Stream.of(
|
||||||
LoginForm(username = "a".repeat(50), password = "a".repeat(72)),
|
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
|
@ParameterizedTest
|
||||||
@@ -53,19 +53,19 @@ internal class UserValidationsTest {
|
|||||||
RegisterForm(username = "", password = ""),
|
RegisterForm(username = "", password = ""),
|
||||||
RegisterForm(username = "a", password = "aaaa"),
|
RegisterForm(username = "a", password = "aaaa"),
|
||||||
RegisterForm(username = "a".repeat(51), password = "a".repeat(8)),
|
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
|
@ParameterizedTest
|
||||||
@MethodSource("invalidRegisterForms")
|
@MethodSource("invalidRegisterForms")
|
||||||
fun `validate invalid register`(form: LoginForm) {
|
fun `validate invalid register`(form: LoginForm) {
|
||||||
assertThat(UserValidations.validateLogin(form), isLeftOfType<InvalidLoginForm>())
|
assertThat(UserValidations.validateLogin(form), isLeftOfType<LoginError.InvalidLoginForm>())
|
||||||
}
|
}
|
||||||
|
|
||||||
@Suppress("Unused")
|
@Suppress("Unused")
|
||||||
fun validRegisterForms(): Stream<RegisterForm> = Stream.of(
|
fun validRegisterForms(): Stream<RegisterForm> = Stream.of(
|
||||||
RegisterForm(username = "a".repeat(50), password = "a".repeat(72)),
|
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
|
@ParameterizedTest
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
org.gradle.jvmargs=-Xmx2048M -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8
|
||||||
|
org.gradle.caching=true
|
||||||
|
org.gradle.parallel=true
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user