Update kotlin libs
This commit is contained in:
@@ -12,7 +12,7 @@ dependencies {
|
||||
implementation(project(":persistence"))
|
||||
implementation(project(":search"))
|
||||
|
||||
api(libs.arrow.core.data)
|
||||
api(libs.arrow.core)
|
||||
api(libs.konform)
|
||||
|
||||
micronaut()
|
||||
|
||||
@@ -9,7 +9,7 @@ import org.apache.commons.compress.archivers.zip.ZipArchiveOutputStream
|
||||
import java.io.ByteArrayInputStream
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.io.InputStream
|
||||
import javax.inject.Singleton
|
||||
import jakarta.inject.Singleton
|
||||
|
||||
interface ExportService {
|
||||
fun exportAsJson(userId: Int): String
|
||||
|
||||
@@ -1,50 +1,47 @@
|
||||
package be.simplenotes.domain
|
||||
|
||||
import arrow.core.Either
|
||||
import arrow.core.computations.either
|
||||
import arrow.core.left
|
||||
import arrow.core.right
|
||||
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
|
||||
import javax.inject.Singleton
|
||||
|
||||
interface MarkdownService {
|
||||
fun renderDocument(input: String): Either<MarkdownParsingError, Document>
|
||||
}
|
||||
|
||||
private typealias MetaMdPair = Pair<String, String>
|
||||
|
||||
@Singleton
|
||||
internal class MarkdownServiceImpl(
|
||||
private val parser: Parser,
|
||||
private val renderer: HtmlRenderer,
|
||||
) : MarkdownService {
|
||||
private val yamlBoundPattern = "-{3}".toRegex()
|
||||
private fun splitMetaFromDocument(input: String): Either<MarkdownParsingError.MissingMeta, MetaMdPair> {
|
||||
private fun splitMetaFromDocument(input: String) = either {
|
||||
val split = input.split(yamlBoundPattern, 3)
|
||||
if (split.size < 3) return MarkdownParsingError.MissingMeta.left()
|
||||
return (split[1].trim() to split[2].trim()).right()
|
||||
ensure(split.size >= 3) { MarkdownParsingError.MissingMeta }
|
||||
(split[1].trim() to split[2].trim())
|
||||
}
|
||||
|
||||
private val yaml = Yaml()
|
||||
private fun parseMeta(input: String): Either<MarkdownParsingError.InvalidMeta, NoteMetadata> {
|
||||
private fun parseMeta(input: String) = either {
|
||||
val load: Map<String, Any> = try {
|
||||
yaml.load(input)
|
||||
} catch (e: ParserException) {
|
||||
return MarkdownParsingError.InvalidMeta.left()
|
||||
raise(MarkdownParsingError.InvalidMeta)
|
||||
} catch (e: ScannerException) {
|
||||
return MarkdownParsingError.InvalidMeta.left()
|
||||
raise(MarkdownParsingError.InvalidMeta)
|
||||
}
|
||||
|
||||
val title = when (val titleNode = load["title"]) {
|
||||
is String, is Number -> titleNode.toString()
|
||||
else -> return MarkdownParsingError.InvalidMeta.left()
|
||||
else -> raise(MarkdownParsingError.InvalidMeta)
|
||||
}
|
||||
|
||||
val tagsNode = load["tags"]
|
||||
@@ -53,15 +50,15 @@ internal class MarkdownServiceImpl(
|
||||
else
|
||||
tagsNode.map { it.toString() }
|
||||
|
||||
return NoteMetadata(title, tags).right()
|
||||
NoteMetadata(title, tags)
|
||||
}
|
||||
|
||||
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()
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
package be.simplenotes.domain
|
||||
|
||||
import arrow.core.computations.either
|
||||
import arrow.core.raise.either
|
||||
import be.simplenotes.domain.security.HtmlSanitizer
|
||||
import be.simplenotes.domain.utils.parseSearchTerms
|
||||
import be.simplenotes.persistence.repositories.NoteRepository
|
||||
@@ -8,12 +8,11 @@ import be.simplenotes.persistence.repositories.UserRepository
|
||||
import be.simplenotes.search.NoteSearcher
|
||||
import be.simplenotes.types.LoggedInUser
|
||||
import be.simplenotes.types.Note
|
||||
import be.simplenotes.types.PersistedNote
|
||||
import be.simplenotes.types.PersistedNoteMetadata
|
||||
import jakarta.annotation.PostConstruct
|
||||
import jakarta.annotation.PreDestroy
|
||||
import jakarta.inject.Singleton
|
||||
import java.util.*
|
||||
import javax.annotation.PostConstruct
|
||||
import javax.annotation.PreDestroy
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Singleton
|
||||
class NoteService(
|
||||
@@ -21,10 +20,10 @@ class NoteService(
|
||||
private val noteRepository: NoteRepository,
|
||||
private val userRepository: UserRepository,
|
||||
private val searcher: NoteSearcher,
|
||||
private val htmlSanitizer: HtmlSanitizer
|
||||
private val htmlSanitizer: HtmlSanitizer,
|
||||
) {
|
||||
|
||||
fun create(user: LoggedInUser, markdownText: String) = either.eager<MarkdownParsingError, PersistedNote> {
|
||||
fun create(user: LoggedInUser, markdownText: String) = either {
|
||||
markdownService.renderDocument(markdownText)
|
||||
.map { it.copy(html = htmlSanitizer.sanitize(user, it.html)) }
|
||||
.map { Note(title = it.metadata.title, tags = it.metadata.tags, markdown = markdownText, html = it.html) }
|
||||
@@ -34,7 +33,7 @@ class NoteService(
|
||||
}
|
||||
|
||||
fun update(user: LoggedInUser, uuid: UUID, markdownText: String) =
|
||||
either.eager<MarkdownParsingError, PersistedNote?> {
|
||||
either {
|
||||
markdownService.renderDocument(markdownText)
|
||||
.map { it.copy(html = htmlSanitizer.sanitize(user, it.html)) }
|
||||
.map {
|
||||
@@ -42,7 +41,7 @@ class NoteService(
|
||||
title = it.metadata.title,
|
||||
tags = it.metadata.tags,
|
||||
markdown = markdownText,
|
||||
html = it.html
|
||||
html = it.html,
|
||||
)
|
||||
}
|
||||
.map { noteRepository.update(user.userId, uuid, it) }
|
||||
|
||||
+22
-28
@@ -1,10 +1,9 @@
|
||||
package be.simplenotes.domain
|
||||
|
||||
import arrow.core.Either
|
||||
import arrow.core.computations.either
|
||||
import arrow.core.filterOrElse
|
||||
import arrow.core.leftIfNull
|
||||
import arrow.core.rightIfNotNull
|
||||
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
|
||||
@@ -13,8 +12,8 @@ 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
|
||||
import javax.inject.Singleton
|
||||
|
||||
interface UserService {
|
||||
fun register(form: RegisterForm): Either<RegisterError, PersistedUser>
|
||||
@@ -30,31 +29,26 @@ internal class UserServiceImpl(
|
||||
private val searcher: NoteSearcher,
|
||||
) : UserService {
|
||||
|
||||
override fun register(form: RegisterForm) = UserValidations.validateRegister(form)
|
||||
.filterOrElse({ !userRepository.exists(it.username) }, { RegisterError.UserExists })
|
||||
.map { it.copy(password = passwordHash.crypt(it.password)) }
|
||||
.map { userRepository.create(it) }
|
||||
.leftIfNull { RegisterError.UserExists }
|
||||
|
||||
override fun login(form: LoginForm) = either.eager<LoginError, Token> {
|
||||
UserValidations.validateLogin(form)
|
||||
.bind()
|
||||
.let { userRepository.find(it.username) }
|
||||
.rightIfNotNull { LoginError.Unregistered }
|
||||
.filterOrElse({ passwordHash.verify(form.password!!, it.password) }, { LoginError.WrongPassword })
|
||||
.map { jwt.sign(LoggedInUser(it)) }
|
||||
.bind()
|
||||
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 delete(form: DeleteForm) = 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 },
|
||||
{ }
|
||||
)
|
||||
!Either.conditionally(userRepository.delete(persistedUser.id), { DeleteError.Unregistered }, { })
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ import com.vladsch.flexmark.html.HtmlRenderer
|
||||
import com.vladsch.flexmark.parser.Parser
|
||||
import com.vladsch.flexmark.util.data.MutableDataSet
|
||||
import io.micronaut.context.annotation.Factory
|
||||
import javax.inject.Singleton
|
||||
import jakarta.inject.Singleton
|
||||
|
||||
@Factory
|
||||
class FlexmarkFactory {
|
||||
@@ -23,11 +23,11 @@ class FlexmarkFactory {
|
||||
set(TaskListExtension.LOOSE_ITEM_CLASS, "")
|
||||
set(
|
||||
TaskListExtension.ITEM_DONE_MARKER,
|
||||
"""<input type="checkbox" checked="checked" disabled="disabled" readonly="readonly" /> """
|
||||
"""<input type="checkbox" checked="checked" disabled="disabled" readonly="readonly" /> """,
|
||||
)
|
||||
set(
|
||||
TaskListExtension.ITEM_NOT_DONE_MARKER,
|
||||
"""<input type="checkbox" disabled="disabled" readonly="readonly" /> """
|
||||
"""<input type="checkbox" disabled="disabled" readonly="readonly" /> """,
|
||||
)
|
||||
set(HtmlRenderer.SOFT_BREAK, "<br>")
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import be.simplenotes.types.LoggedInUser
|
||||
import org.owasp.html.HtmlChangeListener
|
||||
import org.owasp.html.HtmlPolicyBuilder
|
||||
import org.slf4j.LoggerFactory
|
||||
import javax.inject.Singleton
|
||||
import jakarta.inject.Singleton
|
||||
|
||||
@Singleton
|
||||
class HtmlSanitizer {
|
||||
|
||||
@@ -3,7 +3,7 @@ package be.simplenotes.domain.security
|
||||
import be.simplenotes.types.LoggedInUser
|
||||
import com.auth0.jwt.JWTCreator
|
||||
import com.auth0.jwt.interfaces.DecodedJWT
|
||||
import javax.inject.Singleton
|
||||
import jakarta.inject.Singleton
|
||||
|
||||
interface JwtMapper<T> {
|
||||
fun extract(decodedJWT: DecodedJWT): T?
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
package be.simplenotes.domain.security
|
||||
|
||||
import org.mindrot.jbcrypt.BCrypt
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
import jakarta.inject.Inject
|
||||
import jakarta.inject.Singleton
|
||||
|
||||
internal interface PasswordHash {
|
||||
fun crypt(password: String): String
|
||||
|
||||
@@ -7,7 +7,7 @@ import com.auth0.jwt.algorithms.Algorithm
|
||||
import com.auth0.jwt.exceptions.JWTVerificationException
|
||||
import java.util.*
|
||||
import java.util.concurrent.TimeUnit
|
||||
import javax.inject.Singleton
|
||||
import jakarta.inject.Singleton
|
||||
|
||||
@Singleton
|
||||
class SimpleJwt<T>(jwtConfig: JwtConfig, private val mapper: JwtMapper<T>) {
|
||||
|
||||
@@ -89,6 +89,6 @@ internal fun parseSearchTerms(input: String): SearchTerms {
|
||||
title = title,
|
||||
tag = tag,
|
||||
content = content,
|
||||
all = all
|
||||
all = all,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
package be.simplenotes.domain.validation
|
||||
|
||||
import arrow.core.Either
|
||||
import arrow.core.left
|
||||
import arrow.core.right
|
||||
import arrow.core.raise.either
|
||||
import arrow.core.raise.ensure
|
||||
import be.simplenotes.domain.*
|
||||
import be.simplenotes.types.User
|
||||
import io.konform.validation.Validation
|
||||
@@ -21,16 +20,16 @@ internal object UserValidations {
|
||||
}
|
||||
}
|
||||
|
||||
fun validateLogin(form: LoginForm): Either<LoginError.InvalidLoginForm, User> {
|
||||
fun validateLogin(form: LoginForm) = either<LoginError.InvalidLoginForm, User> {
|
||||
val errors = loginValidator.validate(form).errors
|
||||
return if (errors.isEmpty()) User(form.username!!, form.password!!).right()
|
||||
else return LoginError.InvalidLoginForm(errors).left()
|
||||
ensure(errors.isEmpty()) { LoginError.InvalidLoginForm(errors) }
|
||||
User(form.username!!, form.password!!)
|
||||
}
|
||||
|
||||
fun validateRegister(form: RegisterForm): Either<RegisterError.InvalidRegisterForm, User> {
|
||||
fun validateRegister(form: RegisterForm) = either {
|
||||
val errors = loginValidator.validate(form).errors
|
||||
return if (errors.isEmpty()) User(form.username!!, form.password!!).right()
|
||||
else return RegisterError.InvalidRegisterForm(errors).left()
|
||||
ensure(errors.isEmpty()) { RegisterError.InvalidRegisterForm(errors) }
|
||||
User(form.username!!, form.password!!)
|
||||
}
|
||||
|
||||
private val deleteValidator = Validation<DeleteForm> {
|
||||
@@ -47,9 +46,9 @@ internal object UserValidations {
|
||||
}
|
||||
}
|
||||
|
||||
fun validateDelete(form: DeleteForm): Either<DeleteError.InvalidForm, User> {
|
||||
fun validateDelete(form: DeleteForm) = either {
|
||||
val errors = deleteValidator.validate(form).errors
|
||||
return if (errors.isEmpty()) User(form.username!!, form.password!!).right()
|
||||
else return DeleteError.InvalidForm(errors).left()
|
||||
ensure(errors.isEmpty()) { DeleteError.InvalidForm(errors) }
|
||||
User(form.username!!, form.password!!)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,7 +33,7 @@ internal class LoggedInUserExtractorTest {
|
||||
createToken(),
|
||||
createToken(username = "user", id = 1, secret = "not the correct secret"),
|
||||
createToken(username = "user", id = 1) + "\"efesfsef",
|
||||
"something that is not even a token"
|
||||
"something that is not even a token",
|
||||
)
|
||||
|
||||
@ParameterizedTest(name = "[{index}] token `{0}` should be invalid")
|
||||
|
||||
@@ -21,7 +21,7 @@ fun isRight() = object : Matcher<Either<*, *>> {
|
||||
override fun invoke(actual: Either<*, *>) = when (actual) {
|
||||
is Either.Right -> MatchResult.Match
|
||||
is Either.Left -> {
|
||||
val valueA = actual.a
|
||||
val valueA = actual.value
|
||||
MatchResult.Mismatch("is Either.Left<${if (valueA == null) "Null" else valueA::class.simpleName}>")
|
||||
}
|
||||
}
|
||||
@@ -34,7 +34,7 @@ inline fun <reified A> isLeftOfType() = object : Matcher<Either<*, *>> {
|
||||
override fun invoke(actual: Either<*, *>) = when (actual) {
|
||||
is Either.Right -> MatchResult.Mismatch("was Either.Right<>")
|
||||
is Either.Left -> {
|
||||
val valueA = actual.a
|
||||
val valueA = actual.value
|
||||
if (valueA is A) MatchResult.Match
|
||||
else MatchResult.Mismatch("was Left<${if (valueA == null) "Null" else valueA::class.simpleName}>")
|
||||
}
|
||||
|
||||
@@ -33,7 +33,7 @@ internal class SearchTermsParserKtTest {
|
||||
"tag:'example abc' title:'other with words' this is the end ",
|
||||
title = "other with words",
|
||||
tag = "example abc",
|
||||
all = "this is the end"
|
||||
all = "this is the end",
|
||||
),
|
||||
createResult("tag:blah", tag = "blah"),
|
||||
createResult("tag:'some words'", tag = "some words"),
|
||||
@@ -41,7 +41,7 @@ internal class SearchTermsParserKtTest {
|
||||
createResult(
|
||||
"tag:'double quote inside single \" ' global",
|
||||
tag = "double quote inside single \" ",
|
||||
all = "global"
|
||||
all = "global",
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ internal class UserValidationsTest {
|
||||
LoginForm(username = "", password = ""),
|
||||
LoginForm(username = "a", password = "aaaa"),
|
||||
LoginForm(username = "a".repeat(51), password = "a".repeat(8)),
|
||||
LoginForm(username = "a".repeat(10), password = "a".repeat(7))
|
||||
LoginForm(username = "a".repeat(10), password = "a".repeat(7)),
|
||||
)
|
||||
|
||||
@ParameterizedTest
|
||||
@@ -34,7 +34,7 @@ internal class UserValidationsTest {
|
||||
@Suppress("Unused")
|
||||
fun validLoginForms(): Stream<LoginForm> = Stream.of(
|
||||
LoginForm(username = "a".repeat(50), password = "a".repeat(72)),
|
||||
LoginForm(username = "a".repeat(3), password = "a".repeat(8))
|
||||
LoginForm(username = "a".repeat(3), password = "a".repeat(8)),
|
||||
)
|
||||
|
||||
@ParameterizedTest
|
||||
@@ -53,7 +53,7 @@ internal class UserValidationsTest {
|
||||
RegisterForm(username = "", password = ""),
|
||||
RegisterForm(username = "a", password = "aaaa"),
|
||||
RegisterForm(username = "a".repeat(51), password = "a".repeat(8)),
|
||||
RegisterForm(username = "a".repeat(10), password = "a".repeat(7))
|
||||
RegisterForm(username = "a".repeat(10), password = "a".repeat(7)),
|
||||
)
|
||||
|
||||
@ParameterizedTest
|
||||
@@ -65,7 +65,7 @@ internal class UserValidationsTest {
|
||||
@Suppress("Unused")
|
||||
fun validRegisterForms(): Stream<RegisterForm> = Stream.of(
|
||||
RegisterForm(username = "a".repeat(50), password = "a".repeat(72)),
|
||||
RegisterForm(username = "a".repeat(3), password = "a".repeat(8))
|
||||
RegisterForm(username = "a".repeat(3), password = "a".repeat(8)),
|
||||
)
|
||||
|
||||
@ParameterizedTest
|
||||
|
||||
Reference in New Issue
Block a user