Compare commits
4 Commits
235e8b6e3c
...
df8f2d37a1
| Author | SHA1 | Date | |
|---|---|---|---|
| df8f2d37a1 | |||
| 724fa0483e | |||
| f2bdc8d6c7 | |||
| 5aa2e80c5f |
@ -13,4 +13,6 @@ insert_final_newline = true
|
||||
indent_size = 4
|
||||
insert_final_newline = true
|
||||
max_line_length = 120
|
||||
disabled_rules = no-wildcard-imports,import-ordering
|
||||
ktlint_standard_no-wildcard-imports = disabled
|
||||
ktlint_standard_import-ordering = disabled
|
||||
ktlint_standard_multiline-if-else = disabled
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
FROM openjdk:15-alpine as jdkbuilder
|
||||
FROM eclipse-temurin:19-alpine as jdkbuilder
|
||||
|
||||
RUN apk add --no-cache binutils
|
||||
|
||||
ENV MODULES java.base,java.compiler,java.desktop,java.instrument,java.logging,java.management,java.naming,java.scripting,java.security.jgss,java.sql,java.sql.rowset,java.transaction.xa,java.xml,jdk.net
|
||||
ENV MODULES java.base,java.compiler,java.desktop,java.instrument,java.logging,java.management,java.naming,java.scripting,java.security.jgss,java.sql,java.sql.rowset,java.transaction.xa,java.xml,jdk.net,jdk.zipfs
|
||||
|
||||
RUN jlink --output /myjdk --module-path $JAVA_HOME/jmods --add-modules $MODULES --no-header-files --no-man-pages --strip-debug --compress=2
|
||||
|
||||
|
||||
@ -14,6 +14,7 @@ dependencies {
|
||||
implementation(project(":css"))
|
||||
|
||||
implementation(libs.http4k.core)
|
||||
implementation(libs.http4k.multipart)
|
||||
implementation(libs.bundles.jetty)
|
||||
implementation(libs.kotlinx.serialization.json)
|
||||
|
||||
|
||||
@ -1,10 +1,10 @@
|
||||
package be.simplenotes.app
|
||||
|
||||
import jakarta.annotation.PostConstruct
|
||||
import jakarta.annotation.PreDestroy
|
||||
import jakarta.inject.Singleton
|
||||
import org.http4k.server.Http4kServer
|
||||
import org.slf4j.LoggerFactory
|
||||
import javax.annotation.PostConstruct
|
||||
import javax.annotation.PreDestroy
|
||||
import javax.inject.Singleton
|
||||
import be.simplenotes.config.ServerConfig as SimpleNotesServerConfig
|
||||
|
||||
@Singleton
|
||||
|
||||
@ -16,7 +16,7 @@ import org.http4k.core.Status.Companion.OK
|
||||
import org.http4k.lens.Path
|
||||
import org.http4k.lens.uuid
|
||||
import java.util.*
|
||||
import javax.inject.Singleton
|
||||
import jakarta.inject.Singleton
|
||||
|
||||
@Singleton
|
||||
class ApiNoteController(
|
||||
@ -28,7 +28,7 @@ class ApiNoteController(
|
||||
val content = noteContentLens(request)
|
||||
return noteService.create(loggedInUser, content).fold(
|
||||
{ Response(BAD_REQUEST) },
|
||||
{ uuidContentLens(UuidContent(it.uuid), Response(OK)) }
|
||||
{ uuidContentLens(UuidContent(it.uuid), Response(OK)) },
|
||||
)
|
||||
}
|
||||
|
||||
@ -51,7 +51,7 @@ class ApiNoteController(
|
||||
{
|
||||
if (it == null) Response(NOT_FOUND)
|
||||
else Response(OK)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@ -9,7 +9,7 @@ import org.http4k.core.Request
|
||||
import org.http4k.core.Response
|
||||
import org.http4k.core.Status.Companion.BAD_REQUEST
|
||||
import org.http4k.core.Status.Companion.OK
|
||||
import javax.inject.Singleton
|
||||
import jakarta.inject.Singleton
|
||||
|
||||
@Singleton
|
||||
class ApiUserController(
|
||||
@ -23,7 +23,7 @@ class ApiUserController(
|
||||
.login(loginFormLens(request))
|
||||
.fold(
|
||||
{ Response(BAD_REQUEST) },
|
||||
{ tokenLens(Token(it), Response(OK)) }
|
||||
{ tokenLens(Token(it), Response(OK)) },
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@ -6,7 +6,7 @@ import be.simplenotes.views.BaseView
|
||||
import org.http4k.core.Request
|
||||
import org.http4k.core.Response
|
||||
import org.http4k.core.Status.Companion.OK
|
||||
import javax.inject.Singleton
|
||||
import jakarta.inject.Singleton
|
||||
|
||||
@Singleton
|
||||
class BaseController(private val view: BaseView) {
|
||||
|
||||
@ -15,7 +15,7 @@ import org.http4k.core.Status.Companion.OK
|
||||
import org.http4k.core.body.form
|
||||
import org.http4k.routing.path
|
||||
import java.util.*
|
||||
import javax.inject.Singleton
|
||||
import jakarta.inject.Singleton
|
||||
import kotlin.math.abs
|
||||
|
||||
@Singleton
|
||||
@ -35,24 +35,24 @@ class NoteController(
|
||||
MarkdownParsingError.MissingMeta -> view.noteEditor(
|
||||
loggedInUser,
|
||||
error = "Missing note metadata",
|
||||
textarea = markdownForm
|
||||
textarea = markdownForm,
|
||||
)
|
||||
MarkdownParsingError.InvalidMeta -> view.noteEditor(
|
||||
loggedInUser,
|
||||
error = "Invalid note metadata",
|
||||
textarea = markdownForm
|
||||
textarea = markdownForm,
|
||||
)
|
||||
is MarkdownParsingError.ValidationError -> view.noteEditor(
|
||||
loggedInUser,
|
||||
validationErrors = it.validationErrors,
|
||||
textarea = markdownForm
|
||||
textarea = markdownForm,
|
||||
)
|
||||
}
|
||||
Response(BAD_REQUEST).html(html)
|
||||
},
|
||||
{
|
||||
Response.redirect("/notes/${it.uuid}")
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@ -114,24 +114,24 @@ class NoteController(
|
||||
MarkdownParsingError.MissingMeta -> view.noteEditor(
|
||||
loggedInUser,
|
||||
error = "Missing note metadata",
|
||||
textarea = markdownForm
|
||||
textarea = markdownForm,
|
||||
)
|
||||
MarkdownParsingError.InvalidMeta -> view.noteEditor(
|
||||
loggedInUser,
|
||||
error = "Invalid note metadata",
|
||||
textarea = markdownForm
|
||||
textarea = markdownForm,
|
||||
)
|
||||
is MarkdownParsingError.ValidationError -> view.noteEditor(
|
||||
loggedInUser,
|
||||
validationErrors = it.validationErrors,
|
||||
textarea = markdownForm
|
||||
textarea = markdownForm,
|
||||
)
|
||||
}
|
||||
Response(BAD_REQUEST).html(html)
|
||||
},
|
||||
{
|
||||
Response.redirect("/notes/${note.uuid}")
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@ -2,21 +2,19 @@ package be.simplenotes.app.controllers
|
||||
|
||||
import be.simplenotes.app.extensions.html
|
||||
import be.simplenotes.app.extensions.redirect
|
||||
import be.simplenotes.domain.DeleteError
|
||||
import be.simplenotes.domain.DeleteForm
|
||||
import be.simplenotes.domain.ExportService
|
||||
import be.simplenotes.domain.UserService
|
||||
import be.simplenotes.domain.*
|
||||
import be.simplenotes.types.LoggedInUser
|
||||
import be.simplenotes.views.SettingView
|
||||
import jakarta.inject.Singleton
|
||||
import org.http4k.core.*
|
||||
import org.http4k.core.body.form
|
||||
import org.http4k.core.cookie.invalidateCookie
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Singleton
|
||||
class SettingsController(
|
||||
private val userService: UserService,
|
||||
private val exportService: ExportService,
|
||||
private val importService: ImportService,
|
||||
private val settingView: SettingView,
|
||||
) {
|
||||
fun settings(request: Request, loggedInUser: LoggedInUser): Response {
|
||||
@ -33,20 +31,21 @@ class SettingsController(
|
||||
DeleteError.WrongPassword -> Response(Status.OK).html(
|
||||
settingView.settings(
|
||||
loggedInUser,
|
||||
error = "Wrong password"
|
||||
)
|
||||
error = "Wrong password",
|
||||
),
|
||||
)
|
||||
|
||||
is DeleteError.InvalidForm -> Response(Status.OK).html(
|
||||
settingView.settings(
|
||||
loggedInUser,
|
||||
validationErrors = it.validationErrors
|
||||
)
|
||||
validationErrors = it.validationErrors,
|
||||
),
|
||||
)
|
||||
}
|
||||
},
|
||||
{
|
||||
Response.redirect("/").invalidateCookie("Bearer")
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@ -73,10 +72,18 @@ class SettingsController(
|
||||
.body(exportService.exportAsJson(loggedInUser.userId))
|
||||
} else Response(Status.OK).body(exportService.exportAsJson(loggedInUser.userId)).header(
|
||||
"Content-Type",
|
||||
"application/json"
|
||||
"application/json",
|
||||
)
|
||||
}
|
||||
|
||||
private fun Request.deleteForm(loggedInUser: LoggedInUser) =
|
||||
DeleteForm(loggedInUser.username, form("password"), form("checked") != null)
|
||||
|
||||
fun import(request: Request, loggedInUser: LoggedInUser): Response {
|
||||
val form = MultipartFormBody.from(request)
|
||||
val file = form.file("file") ?: return Response(Status.BAD_REQUEST)
|
||||
val json = file.content.bufferedReader().readText()
|
||||
importService.importJson(loggedInUser, json)
|
||||
return Response.redirect("/notes")
|
||||
}
|
||||
}
|
||||
|
||||
@ -17,7 +17,7 @@ import org.http4k.core.cookie.SameSite
|
||||
import org.http4k.core.cookie.cookie
|
||||
import org.http4k.core.cookie.invalidateCookie
|
||||
import java.util.concurrent.TimeUnit
|
||||
import javax.inject.Singleton
|
||||
import jakarta.inject.Singleton
|
||||
|
||||
@Singleton
|
||||
class UserController(
|
||||
@ -27,7 +27,7 @@ class UserController(
|
||||
) {
|
||||
fun register(request: Request, loggedInUser: LoggedInUser?): Response {
|
||||
if (request.method == GET) return Response(OK).html(
|
||||
userView.register(loggedInUser)
|
||||
userView.register(loggedInUser),
|
||||
)
|
||||
|
||||
val result = userService.register(request.registerForm())
|
||||
@ -37,19 +37,19 @@ class UserController(
|
||||
val html = when (it) {
|
||||
RegisterError.UserExists -> userView.register(
|
||||
loggedInUser,
|
||||
error = "User already exists"
|
||||
error = "User already exists",
|
||||
)
|
||||
is RegisterError.InvalidRegisterForm ->
|
||||
userView.register(
|
||||
loggedInUser,
|
||||
validationErrors = it.validationErrors
|
||||
validationErrors = it.validationErrors,
|
||||
)
|
||||
}
|
||||
Response(OK).html(html)
|
||||
},
|
||||
{
|
||||
Response.redirect("/login")
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@ -58,7 +58,7 @@ class UserController(
|
||||
|
||||
fun login(request: Request, loggedInUser: LoggedInUser?): Response {
|
||||
if (request.method == GET) return Response(OK).html(
|
||||
userView.login(loggedInUser)
|
||||
userView.login(loggedInUser),
|
||||
)
|
||||
|
||||
val result = userService.login(request.loginForm())
|
||||
@ -69,24 +69,24 @@ class UserController(
|
||||
LoginError.Unregistered ->
|
||||
userView.login(
|
||||
loggedInUser,
|
||||
error = "User does not exist"
|
||||
error = "User does not exist",
|
||||
)
|
||||
LoginError.WrongPassword ->
|
||||
userView.login(
|
||||
loggedInUser,
|
||||
error = "Wrong password"
|
||||
error = "Wrong password",
|
||||
)
|
||||
is LoginError.InvalidLoginForm ->
|
||||
userView.login(
|
||||
loggedInUser,
|
||||
validationErrors = it.validationErrors
|
||||
validationErrors = it.validationErrors,
|
||||
)
|
||||
}
|
||||
Response(OK).html(html)
|
||||
},
|
||||
{ token ->
|
||||
Response.redirect("/notes").loginCookie(token, request.isSecure())
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@ -101,8 +101,8 @@ class UserController(
|
||||
httpOnly = true,
|
||||
sameSite = SameSite.Lax,
|
||||
maxAge = validityInSeconds,
|
||||
secure = secure
|
||||
)
|
||||
secure = secure,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@ -24,13 +24,13 @@ fun Request.isSecure() = header("X-Forwarded-Proto")?.contains("https") ?: false
|
||||
val bodyLens = httpBodyRoot(
|
||||
listOf(Meta(true, "body", ParamMeta.ObjectParam, "body")),
|
||||
ContentType.APPLICATION_JSON.withNoDirectives(),
|
||||
ContentNegotiation.StrictNoDirective
|
||||
ContentNegotiation.StrictNoDirective,
|
||||
).map(
|
||||
{ it.payload.asString() },
|
||||
{ Body(it) }
|
||||
{ Body(it) },
|
||||
)
|
||||
|
||||
inline fun <reified T> Json.auto(): BiDiBodyLensSpec<T> = bodyLens.map(
|
||||
{ decodeFromString(it) },
|
||||
{ encodeToString(it) }
|
||||
{ encodeToString(it) },
|
||||
)
|
||||
|
||||
@ -10,7 +10,7 @@ import org.http4k.core.Status.Companion.NOT_IMPLEMENTED
|
||||
import org.http4k.core.Status.Companion.SERVICE_UNAVAILABLE
|
||||
import org.slf4j.LoggerFactory
|
||||
import java.sql.SQLTransientException
|
||||
import javax.inject.Singleton
|
||||
import jakarta.inject.Singleton
|
||||
|
||||
@Singleton
|
||||
class ErrorFilter(private val errorView: ErrorView) : Filter {
|
||||
|
||||
@ -8,14 +8,14 @@ import org.eclipse.jetty.servlet.ServletHolder
|
||||
import org.http4k.core.HttpHandler
|
||||
import org.http4k.server.Http4kServer
|
||||
import org.http4k.server.ServerConfig
|
||||
import org.http4k.servlet.asServlet
|
||||
import org.http4k.servlet.jakarta.asServlet
|
||||
|
||||
class Jetty(private val port: Int, private val server: Server) : ServerConfig {
|
||||
constructor(port: Int, vararg inConnectors: ConnectorBuilder) : this(
|
||||
port,
|
||||
Server().apply {
|
||||
inConnectors.forEach { addConnector(it(this)) }
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
override fun toServer(http: HttpHandler): Http4kServer {
|
||||
|
||||
@ -7,8 +7,8 @@ import io.micronaut.context.annotation.Factory
|
||||
import io.micronaut.context.annotation.Primary
|
||||
import org.http4k.core.RequestContexts
|
||||
import org.http4k.lens.RequestContextKey
|
||||
import javax.inject.Named
|
||||
import javax.inject.Singleton
|
||||
import jakarta.inject.Named
|
||||
import jakarta.inject.Singleton
|
||||
|
||||
@Factory
|
||||
class AuthModule {
|
||||
@ -39,7 +39,7 @@ class AuthModule {
|
||||
simpleJwt = simpleJwt,
|
||||
lens = lens,
|
||||
source = JwtSource.Header,
|
||||
redirect = false
|
||||
redirect = false,
|
||||
)
|
||||
|
||||
@Singleton
|
||||
|
||||
@ -7,7 +7,7 @@ import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.modules.SerializersModule
|
||||
import java.time.LocalDateTime
|
||||
import java.util.*
|
||||
import javax.inject.Singleton
|
||||
import jakarta.inject.Singleton
|
||||
|
||||
@Factory
|
||||
class JsonModule {
|
||||
|
||||
@ -9,8 +9,8 @@ import io.micronaut.context.annotation.Factory
|
||||
import org.eclipse.jetty.server.ServerConnector
|
||||
import org.http4k.server.Http4kServer
|
||||
import org.http4k.server.asServer
|
||||
import javax.inject.Named
|
||||
import javax.inject.Singleton
|
||||
import jakarta.inject.Named
|
||||
import jakarta.inject.Singleton
|
||||
import org.eclipse.jetty.server.Server as JettyServer
|
||||
import org.http4k.server.ServerConfig as Http4kServerConfig
|
||||
|
||||
|
||||
@ -12,8 +12,8 @@ import org.http4k.routing.RoutingHttpHandler
|
||||
import org.http4k.routing.bind
|
||||
import org.http4k.routing.routes
|
||||
import java.util.function.Supplier
|
||||
import javax.inject.Named
|
||||
import javax.inject.Singleton
|
||||
import jakarta.inject.Named
|
||||
import jakarta.inject.Singleton
|
||||
|
||||
@Singleton
|
||||
class ApiRoutes(
|
||||
@ -23,7 +23,6 @@ class ApiRoutes(
|
||||
@Named("required") private val authLens: RequiredAuthLens,
|
||||
) : Supplier<RoutingHttpHandler> {
|
||||
override fun get(): RoutingHttpHandler {
|
||||
|
||||
infix fun PathMethod.to(action: ProtectedHandler) =
|
||||
this to { req: Request -> action(req, authLens(req)) }
|
||||
|
||||
@ -38,9 +37,9 @@ class ApiRoutes(
|
||||
"/search" bind POST to ::search,
|
||||
"/{uuid}" bind GET to ::note,
|
||||
"/{uuid}" bind PUT to ::update,
|
||||
)
|
||||
),
|
||||
).withBasePath("/notes")
|
||||
}
|
||||
},
|
||||
|
||||
).withBasePath("/api")
|
||||
}
|
||||
|
||||
@ -13,8 +13,8 @@ import org.http4k.core.Request
|
||||
import org.http4k.core.then
|
||||
import org.http4k.routing.*
|
||||
import java.util.function.Supplier
|
||||
import javax.inject.Named
|
||||
import javax.inject.Singleton
|
||||
import jakarta.inject.Named
|
||||
import jakarta.inject.Singleton
|
||||
|
||||
@Singleton
|
||||
class BasicRoutes(
|
||||
@ -26,7 +26,6 @@ class BasicRoutes(
|
||||
) : Supplier<RoutingHttpHandler> {
|
||||
|
||||
override fun get(): RoutingHttpHandler {
|
||||
|
||||
infix fun PathMethod.to(action: PublicHandler) =
|
||||
this to { req: Request -> action(req, authLens(req)) }
|
||||
|
||||
@ -34,8 +33,8 @@ class BasicRoutes(
|
||||
static(
|
||||
ResourceLoader.Classpath("/static"),
|
||||
"woff2" to ContentType("font/woff2"),
|
||||
"webmanifest" to ContentType("application/manifest+json")
|
||||
)
|
||||
"webmanifest" to ContentType("application/manifest+json"),
|
||||
),
|
||||
)
|
||||
|
||||
return routes(
|
||||
@ -48,9 +47,9 @@ class BasicRoutes(
|
||||
"/login" bind POST to userCtrl::login,
|
||||
"/logout" bind POST to userCtrl::logout,
|
||||
"/notes/public/{uuid}" bind GET to noteCtrl::public,
|
||||
)
|
||||
),
|
||||
),
|
||||
staticHandler
|
||||
staticHandler,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -12,8 +12,8 @@ import org.http4k.routing.RoutingHttpHandler
|
||||
import org.http4k.routing.bind
|
||||
import org.http4k.routing.routes
|
||||
import java.util.function.Supplier
|
||||
import javax.inject.Named
|
||||
import javax.inject.Singleton
|
||||
import jakarta.inject.Named
|
||||
import jakarta.inject.Singleton
|
||||
|
||||
@Singleton
|
||||
class NoteRoutes(
|
||||
@ -22,7 +22,6 @@ class NoteRoutes(
|
||||
@Named("required") private val authLens: RequiredAuthLens,
|
||||
) : Supplier<RoutingHttpHandler> {
|
||||
override fun get(): RoutingHttpHandler {
|
||||
|
||||
infix fun PathMethod.to(action: ProtectedHandler) =
|
||||
this to { req: Request -> action(req, authLens(req)) }
|
||||
|
||||
@ -40,7 +39,7 @@ class NoteRoutes(
|
||||
"/{uuid}/edit" bind POST to ::edit,
|
||||
"/deleted/{uuid}" bind POST to ::deleted,
|
||||
).withBasePath("/notes")
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -9,7 +9,7 @@ import org.http4k.filter.ServerFilters.InitialiseRequestContext
|
||||
import org.http4k.routing.RoutingHttpHandler
|
||||
import org.http4k.routing.routes
|
||||
import java.util.function.Supplier
|
||||
import javax.inject.Singleton
|
||||
import jakarta.inject.Singleton
|
||||
|
||||
@Singleton
|
||||
class Router(
|
||||
@ -18,9 +18,8 @@ class Router(
|
||||
private val subRouters: List<Supplier<RoutingHttpHandler>>,
|
||||
) {
|
||||
operator fun invoke(): RoutingHttpHandler {
|
||||
|
||||
val routes = routes(
|
||||
*subRouters.map { it.get() }.toTypedArray()
|
||||
*subRouters.map { it.get() }.toTypedArray(),
|
||||
)
|
||||
|
||||
return errorFilter
|
||||
|
||||
@ -12,8 +12,8 @@ import org.http4k.routing.RoutingHttpHandler
|
||||
import org.http4k.routing.bind
|
||||
import org.http4k.routing.routes
|
||||
import java.util.function.Supplier
|
||||
import javax.inject.Named
|
||||
import javax.inject.Singleton
|
||||
import jakarta.inject.Named
|
||||
import jakarta.inject.Singleton
|
||||
|
||||
@Singleton
|
||||
class SettingsRoutes(
|
||||
@ -22,7 +22,6 @@ class SettingsRoutes(
|
||||
@Named("required") private val authLens: RequiredAuthLens,
|
||||
) : Supplier<RoutingHttpHandler> {
|
||||
override fun get(): RoutingHttpHandler {
|
||||
|
||||
infix fun PathMethod.to(action: ProtectedHandler) =
|
||||
this to { req: Request -> action(req, authLens(req)) }
|
||||
|
||||
@ -31,7 +30,8 @@ class SettingsRoutes(
|
||||
"/settings" bind GET to settingsController::settings,
|
||||
"/settings" bind POST to settingsController::settings,
|
||||
"/export" bind POST to settingsController::export,
|
||||
)
|
||||
"/import" bind POST to settingsController::import,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -17,6 +17,6 @@ internal class LocalDateTimeSerializer : KSerializer<LocalDateTime> {
|
||||
}
|
||||
|
||||
override fun deserialize(decoder: Decoder): LocalDateTime {
|
||||
TODO("Not implemented, isn't needed")
|
||||
return LocalDateTime.parse(decoder.decodeString())
|
||||
}
|
||||
}
|
||||
|
||||
@ -3,7 +3,7 @@ package be.simplenotes.app.utils
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.jsonObject
|
||||
import kotlinx.serialization.json.jsonPrimitive
|
||||
import javax.inject.Singleton
|
||||
import jakarta.inject.Singleton
|
||||
|
||||
interface StaticFileResolver {
|
||||
fun resolve(name: String): String?
|
||||
|
||||
@ -58,8 +58,8 @@ internal class RequiredAuthFilterTest {
|
||||
},
|
||||
"/protected" bind GET to requiredAuth.then { request: Request ->
|
||||
Response(OK).body(requiredLens(request).toString())
|
||||
}
|
||||
)
|
||||
},
|
||||
),
|
||||
)
|
||||
// endregion
|
||||
|
||||
|
||||
@ -7,9 +7,9 @@ repositories {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation("org.jetbrains.kotlin:kotlin-gradle-plugin:1.5.0")
|
||||
implementation("org.jetbrains.kotlin:kotlin-serialization:1.5.0")
|
||||
implementation("gradle.plugin.com.github.jengelman.gradle.plugins:shadow:7.0.0")
|
||||
implementation("org.jlleitschuh.gradle:ktlint-gradle:10.0.0")
|
||||
implementation("com.github.ben-manes:gradle-versions-plugin:0.28.0")
|
||||
implementation("org.jetbrains.kotlin:kotlin-gradle-plugin:1.8.21")
|
||||
implementation("org.jetbrains.kotlin:kotlin-serialization:1.8.21")
|
||||
implementation("com.github.johnrengelman:shadow:8.1.1")
|
||||
implementation("com.diffplug.spotless:spotless-plugin-gradle:6.13.0")
|
||||
implementation("com.github.ben-manes:gradle-versions-plugin:0.46.0")
|
||||
}
|
||||
|
||||
@ -5,7 +5,7 @@ package be.simplenotes
|
||||
object Libs {
|
||||
|
||||
object Micronaut {
|
||||
private const val version = "2.5.1"
|
||||
private const val version = "4.0.0-M2"
|
||||
const val inject = "io.micronaut:micronaut-inject:$version"
|
||||
const val processor = "io.micronaut:micronaut-inject-java:$version"
|
||||
}
|
||||
|
||||
@ -2,8 +2,10 @@ package be.simplenotes
|
||||
|
||||
import org.gradle.api.Plugin
|
||||
import org.gradle.api.Project
|
||||
import org.gradle.api.plugins.JavaPluginConvention
|
||||
import org.gradle.api.plugins.JavaPluginExtension
|
||||
import org.gradle.api.tasks.SourceSetContainer
|
||||
import org.gradle.kotlin.dsl.get
|
||||
import org.gradle.kotlin.dsl.getByType
|
||||
import org.gradle.kotlin.dsl.register
|
||||
import java.io.File
|
||||
|
||||
@ -17,7 +19,7 @@ class PostcssPlugin : Plugin<Project> {
|
||||
getByName("processResources").dependsOn("postcss")
|
||||
}
|
||||
|
||||
val sourceSets = project.convention.getPlugin(JavaPluginConvention::class.java).sourceSets
|
||||
val sourceSets = project.extensions.getByType<SourceSetContainer>()
|
||||
val root = File("${project.buildDir}/generated-resources/css")
|
||||
sourceSets["main"].resources.srcDir(root)
|
||||
}
|
||||
|
||||
@ -11,6 +11,10 @@ tasks.withType<ShadowJar> {
|
||||
archiveAppendix.set("with-dependencies")
|
||||
manifest.attributes["Main-Class"] = "be.simplenotes.app.SimpleNotesKt"
|
||||
|
||||
// johnrengelman/shadow#449
|
||||
// we need this for lucene-core
|
||||
manifest.attributes["Multi-Release"] = "true"
|
||||
|
||||
mergeServiceFiles()
|
||||
|
||||
File(rootProject.projectDir, "buildSrc/src/main/resources/exclusions")
|
||||
|
||||
@ -4,5 +4,11 @@ plugins {
|
||||
id("be.simplenotes.java-convention")
|
||||
id("be.simplenotes.kotlin-convention")
|
||||
id("be.simplenotes.junit-convention")
|
||||
id("org.jlleitschuh.gradle.ktlint")
|
||||
id("com.diffplug.spotless")
|
||||
}
|
||||
|
||||
spotless {
|
||||
kotlin {
|
||||
ktlint("0.48.0").setEditorConfigPath(project.rootProject.file(".editorconfig"))
|
||||
}
|
||||
}
|
||||
|
||||
@ -12,8 +12,10 @@ group = "be.simplenotes"
|
||||
version = "1.0-SNAPSHOT"
|
||||
|
||||
java {
|
||||
sourceCompatibility = JavaVersion.VERSION_15
|
||||
targetCompatibility = JavaVersion.VERSION_15
|
||||
toolchain {
|
||||
languageVersion.set(JavaLanguageVersion.of(19))
|
||||
vendor.set(JvmVendorSpec.ORACLE)
|
||||
}
|
||||
}
|
||||
|
||||
tasks.withType<JavaCompile> {
|
||||
|
||||
@ -1,5 +1,7 @@
|
||||
package be.simplenotes
|
||||
|
||||
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
|
||||
import org.jetbrains.kotlin.gradle.dsl.KotlinVersion
|
||||
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
|
||||
|
||||
plugins {
|
||||
@ -13,14 +15,14 @@ dependencies {
|
||||
}
|
||||
|
||||
tasks.withType<KotlinCompile> {
|
||||
kotlinOptions {
|
||||
jvmTarget = "15"
|
||||
javaParameters = true
|
||||
freeCompilerArgs = listOf(
|
||||
compilerOptions {
|
||||
jvmTarget.set(JvmTarget.JVM_19)
|
||||
javaParameters.set(true)
|
||||
freeCompilerArgs.addAll(
|
||||
"-Xinline-classes",
|
||||
"-Xno-param-assertions",
|
||||
"-Xno-call-assertions",
|
||||
"-Xno-receiver-assertions"
|
||||
"-Xno-receiver-assertions",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -10,11 +10,7 @@ tasks.named<DependencyUpdatesTask>("dependencyUpdates").configure {
|
||||
resolutionStrategy {
|
||||
componentSelection {
|
||||
all {
|
||||
if ("RC" in candidate.version) reject("Release candidate")
|
||||
|
||||
when {
|
||||
candidate.group == "org.eclipse.jetty" && candidate.version.startsWith("11.") -> reject("javax -> jakarta")
|
||||
}
|
||||
if ("alpha|beta|rc".toRegex().containsMatchIn(candidate.version.lowercase())) reject("Non stable version")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
META-INF/maven/**
|
||||
META-INF/proguard/**
|
||||
META-INF/com.android.tools/**
|
||||
META-INF/*.kotlin_module
|
||||
META-INF/DEPENDENCIES*
|
||||
META-INF/NOTICE*
|
||||
@ -7,6 +8,7 @@ META-INF/LICENSE*
|
||||
LICENSE*
|
||||
META-INF/README*
|
||||
META-INF/native-image/**
|
||||
**/module-info.**
|
||||
|
||||
# Jetty
|
||||
about.html
|
||||
|
||||
@ -7,6 +7,7 @@ plugins {
|
||||
|
||||
dependencies {
|
||||
micronaut()
|
||||
runtimeOnly(libs.yaml)
|
||||
|
||||
testImplementation(libs.bundles.test)
|
||||
testRuntimeOnly(libs.slf4j.logback)
|
||||
|
||||
@ -3,7 +3,7 @@ package be.simplenotes.config
|
||||
import io.micronaut.context.annotation.*
|
||||
import java.nio.file.Path
|
||||
import java.util.concurrent.TimeUnit
|
||||
import javax.inject.Singleton
|
||||
import jakarta.inject.Singleton
|
||||
|
||||
data class DataConfig(val dataDir: String)
|
||||
|
||||
@ -36,7 +36,7 @@ class ConfigFactory {
|
||||
fun datasourceConfig(dataConfig: DataConfig) = DataSourceConfig(
|
||||
jdbcUrl = "jdbc:h2:" + Path.of(dataConfig.dataDir, "simplenotes").normalize().toAbsolutePath(),
|
||||
maximumPoolSize = 10,
|
||||
connectionTimeout = 1000
|
||||
connectionTimeout = 1000,
|
||||
)
|
||||
|
||||
@Singleton
|
||||
@ -44,7 +44,7 @@ class ConfigFactory {
|
||||
fun testDatasourceConfig() = DataSourceConfig(
|
||||
jdbcUrl = "jdbc:h2:mem:regular;DB_CLOSE_DELAY=-1",
|
||||
maximumPoolSize = 2,
|
||||
connectionTimeout = 1000
|
||||
connectionTimeout = 1000,
|
||||
)
|
||||
|
||||
@Singleton
|
||||
@ -54,5 +54,4 @@ class ConfigFactory {
|
||||
@Singleton
|
||||
@Requires(env = ["test"])
|
||||
fun testDataConfig() = DataConfig("/tmp")
|
||||
|
||||
}
|
||||
|
||||
@ -5,12 +5,11 @@
|
||||
"//": "`gradle css`"
|
||||
},
|
||||
"dependencies": {
|
||||
"autoprefixer": "^9.8.6",
|
||||
"cssnano": "^4.1.10",
|
||||
"postcss-cli": "^7.1.1",
|
||||
"postcss-hash": "^2.0.0",
|
||||
"postcss-import": "^12.0.1",
|
||||
"postcss-nested": "^4.2.3",
|
||||
"tailwindcss": "^1.5.1"
|
||||
"autoprefixer": "^10.4.14",
|
||||
"cssnano": "^6.0.1",
|
||||
"postcss-cli": "^10.1.0",
|
||||
"postcss-hash": "^3.0.0",
|
||||
"postcss-import": "^15.1.0",
|
||||
"tailwindcss": "^3.3.2"
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
module.exports = {
|
||||
plugins: [
|
||||
require('postcss-import'),
|
||||
require('postcss-nested'),
|
||||
require('tailwindcss/nesting'),
|
||||
require('tailwindcss'),
|
||||
require('autoprefixer'),
|
||||
require('cssnano')({
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
@apply font-semibold py-2 px-4 rounded;
|
||||
|
||||
&:focus {
|
||||
@apply outline-none shadow-outline;
|
||||
@apply outline-none ring;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
#note {
|
||||
a {
|
||||
@apply text-blue-700 underline;
|
||||
@apply text-blue-500 underline;
|
||||
}
|
||||
|
||||
p {
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
@apply italic font-semibold text-sm bg-teal-500 text-gray-900 rounded-full py-1 px-2 align-middle;
|
||||
|
||||
&:focus {
|
||||
@apply outline-none shadow-outline bg-teal-800 text-white;
|
||||
@apply outline-none ring bg-teal-800 text-white;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
|
||||
@ -1,9 +1,7 @@
|
||||
module.exports = {
|
||||
purge: {
|
||||
content: [
|
||||
process.env.PURGE
|
||||
]
|
||||
},
|
||||
content: [
|
||||
process.env.PURGE
|
||||
],
|
||||
theme: {
|
||||
fontFamily: {
|
||||
'sans': [
|
||||
|
||||
2255
css/yarn.lock
2255
css/yarn.lock
File diff suppressed because it is too large
Load Diff
@ -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
|
||||
|
||||
30
domain/src/ImportService.kt
Normal file
30
domain/src/ImportService.kt
Normal file
@ -0,0 +1,30 @@
|
||||
package be.simplenotes.domain
|
||||
|
||||
import be.simplenotes.domain.security.HtmlSanitizer
|
||||
import be.simplenotes.persistence.repositories.NoteRepository
|
||||
import be.simplenotes.types.ExportedNote
|
||||
import be.simplenotes.types.LoggedInUser
|
||||
import jakarta.inject.Singleton
|
||||
import kotlinx.serialization.builtins.ListSerializer
|
||||
import kotlinx.serialization.json.Json
|
||||
|
||||
interface ImportService {
|
||||
fun importJson(user: LoggedInUser, content: String)
|
||||
}
|
||||
|
||||
@Singleton
|
||||
internal class ImportServiceImpl(
|
||||
private val noteRepository: NoteRepository,
|
||||
private val json: Json,
|
||||
private val htmlSanitizer: HtmlSanitizer,
|
||||
) : ImportService {
|
||||
override fun importJson(user: LoggedInUser, content: String) {
|
||||
val notes = json.decodeFromString(ListSerializer(ExportedNote.serializer()), content)
|
||||
noteRepository.import(
|
||||
user.userId,
|
||||
notes.map {
|
||||
it.copy(html = htmlSanitizer.sanitize(user, it.html))
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -1,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) }
|
||||
|
||||
@ -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
|
||||
|
||||
@ -1,3 +1,3 @@
|
||||
org.gradle.jvmargs=-Xmx2048M -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8
|
||||
org.gradle.jvmargs=-Xmx2048M -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8
|
||||
org.gradle.caching=true
|
||||
org.gradle.parallel=true
|
||||
|
||||
@ -1,36 +1,37 @@
|
||||
[versions]
|
||||
flexmark = "0.62.2"
|
||||
lucene = "8.8.2"
|
||||
http4k = "4.8.0.0"
|
||||
jetty = "10.0.2"
|
||||
mapstruct = "1.4.2.Final"
|
||||
micronaut-inject = "2.5.1"
|
||||
flexmark = "0.64.4"
|
||||
lucene = "9.5.0"
|
||||
http4k = "4.43.0.0"
|
||||
jetty = "11.0.15"
|
||||
mapstruct = "1.5.5.Final"
|
||||
micronaut-inject = "4.0.0-M2"
|
||||
|
||||
[libraries]
|
||||
flexmark-core = { module = "com.vladsch.flexmark:flexmark", version.ref = "flexmark" }
|
||||
flexmark-tasklist = { module = "com.vladsch.flexmark:flexmark-ext-gfm-tasklist", version.ref = "flexmark" }
|
||||
|
||||
flyway = { module = "org.flywaydb:flyway-core", version = "7.8.2" }
|
||||
hikariCP = { module = "com.zaxxer:HikariCP", version = "4.0.3" }
|
||||
h2 = { module = "com.h2database:h2", version = "1.4.200" }
|
||||
ktorm = { module = "org.ktorm:ktorm-core", version = "3.3.0" }
|
||||
flyway = { module = "org.flywaydb:flyway-core", version = "9.17.0" }
|
||||
hikariCP = { module = "com.zaxxer:HikariCP", version = "5.0.1" }
|
||||
h2 = { module = "com.h2database:h2", version = "2.1.214" }
|
||||
ktorm = { module = "org.ktorm:ktorm-core", version = "3.6.0" }
|
||||
|
||||
lucene-core = { module = "org.apache.lucene:lucene-core", version.ref = "lucene" }
|
||||
lucene-analyzers-common = { module = "org.apache.lucene:lucene-analyzers-common", version.ref = "lucene" }
|
||||
lucene-analyzers-common = { module = "org.apache.lucene:lucene-analysis-common", version.ref = "lucene" }
|
||||
lucene-queryparser = { module = "org.apache.lucene:lucene-queryparser", version.ref = "lucene" }
|
||||
|
||||
http4k-core = { module = "org.http4k:http4k-core", version.ref = "http4k" }
|
||||
http4k-multipart = { module = "org.http4k:http4k-multipart", version.ref = "http4k" }
|
||||
http4k-testing-hamkrest = { module = "org.http4k:http4k-testing-hamkrest", version.ref = "http4k" }
|
||||
|
||||
jetty-server = { module = "org.eclipse.jetty:jetty-server", version.ref = "jetty" }
|
||||
jetty-servlet = { module = "org.eclipse.jetty:jetty-servlet", version.ref = "jetty" }
|
||||
javax-servlet = { module = "javax.servlet:javax.servlet-api", version = "4.0.1" }
|
||||
javax-servlet = { module = "jakarta.servlet:jakarta.servlet-api", version = "6.0.0" }
|
||||
|
||||
kotlinx-html = { module = "org.jetbrains.kotlinx:kotlinx-html-jvm", version = "0.7.3" }
|
||||
kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json-jvm", version = "1.2.0" }
|
||||
kotlinx-html = { module = "org.jetbrains.kotlinx:kotlinx-html-jvm", version = "0.8.1" }
|
||||
kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json-jvm", version = "1.5.0" }
|
||||
|
||||
slf4j-api = { module = "org.slf4j:slf4j-api", version = "2.0.0-alpha1" }
|
||||
slf4j-logback = { module = "ch.qos.logback:logback-classic", version = "1.3.0-alpha5" }
|
||||
slf4j-api = { module = "org.slf4j:slf4j-api", version = "2.0.7" }
|
||||
slf4j-logback = { module = "ch.qos.logback:logback-classic", version = "1.4.7" }
|
||||
|
||||
mapstruct-core = { module = "org.mapstruct:mapstruct", version.ref = "mapstruct" }
|
||||
mapstruct-processor = { module = "org.mapstruct:mapstruct-processor", version.ref = "mapstruct" }
|
||||
@ -38,19 +39,19 @@ mapstruct-processor = { module = "org.mapstruct:mapstruct-processor", version.re
|
||||
micronaut-inject = { module = "io.micronaut:micronaut-inject", version.ref = "micronaut-inject" }
|
||||
micronaut-processor = { module = "io.micronaut:micronaut-inject-java", version.ref = "micronaut-inject" }
|
||||
|
||||
arrow-core-data = { module = "io.arrow-kt:arrow-core-data", version = "0.11.0" }
|
||||
commonsCompress = { module = "org.apache.commons:commons-compress", version = "1.20" }
|
||||
jwt = { module = "com.auth0:java-jwt", version = "3.15.0" }
|
||||
arrow-core = { module = "io.arrow-kt:arrow-core", version = "1.2.0-RC" }
|
||||
commonsCompress = { module = "org.apache.commons:commons-compress", version = "1.23.0" }
|
||||
jwt = { module = "com.auth0:java-jwt", version = "4.4.0" }
|
||||
bcrypt = { module = "org.mindrot:jbcrypt", version = "0.4" }
|
||||
konform = { module = "io.konform:konform-jvm", version = "0.2.0" }
|
||||
owasp-html-sanitizer = { module = "com.googlecode.owasp-java-html-sanitizer:owasp-java-html-sanitizer", version = "20200713.1" }
|
||||
prettytime = { module = "org.ocpsoft.prettytime:prettytime", version = "5.0.1.Final" }
|
||||
yaml = { module = "org.yaml:snakeyaml", version = "1.28" }
|
||||
konform = { module = "io.konform:konform-jvm", version = "0.4.0" }
|
||||
owasp-html-sanitizer = { module = "com.googlecode.owasp-java-html-sanitizer:owasp-java-html-sanitizer", version = "20220608.1" }
|
||||
prettytime = { module = "org.ocpsoft.prettytime:prettytime", version = "5.0.6.Final" }
|
||||
yaml = { module = "org.yaml:snakeyaml", version = "2.0" }
|
||||
|
||||
assertJ = { module = "org.assertj:assertj-core", version = "3.19.0" }
|
||||
assertJ = { module = "org.assertj:assertj-core", version = "3.24.2" }
|
||||
hamkrest = { module = "com.natpryce:hamkrest", version = "1.8.0.1" }
|
||||
junit-jupiter = { module = "org.junit.jupiter:junit-jupiter", version = "5.7.1" }
|
||||
mockk = { module = "io.mockk:mockk", version = "1.11.0" }
|
||||
junit-jupiter = { module = "org.junit.jupiter:junit-jupiter", version = "5.9.3" }
|
||||
mockk = { module = "io.mockk:mockk", version = "1.13.5" }
|
||||
faker = { module = "com.github.javafaker:javafaker", version = "1.0.2" }
|
||||
|
||||
[bundles]
|
||||
|
||||
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
Binary file not shown.
3
gradle/wrapper/gradle-wrapper.properties
vendored
3
gradle/wrapper/gradle-wrapper.properties
vendored
@ -1,5 +1,6 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.0-bin.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.1.1-bin.zip
|
||||
networkTimeout=10000
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
package be.simplenotes.persistence
|
||||
|
||||
import org.flywaydb.core.Flyway
|
||||
import javax.inject.Singleton
|
||||
import jakarta.inject.Singleton
|
||||
import javax.sql.DataSource
|
||||
|
||||
interface DbMigrations {
|
||||
@ -14,6 +14,7 @@ internal class DbMigrationsImpl(private val dataSource: DataSource) : DbMigratio
|
||||
Flyway.configure()
|
||||
.dataSource(dataSource)
|
||||
.locations("db/migration")
|
||||
.loggers("slf4j")
|
||||
.load()
|
||||
.migrate()
|
||||
}
|
||||
|
||||
@ -8,7 +8,7 @@ import io.micronaut.context.annotation.Bean
|
||||
import io.micronaut.context.annotation.Factory
|
||||
import org.ktorm.database.Database
|
||||
import org.ktorm.database.SqlDialect
|
||||
import javax.inject.Singleton
|
||||
import jakarta.inject.Singleton
|
||||
import javax.sql.DataSource
|
||||
|
||||
@Factory
|
||||
@ -17,10 +17,13 @@ class PersistenceModule {
|
||||
@Singleton
|
||||
internal fun database(migrations: DbMigrations, dataSource: DataSource): Database {
|
||||
migrations.migrate()
|
||||
return Database.connect(dataSource, dialect = object : SqlDialect {
|
||||
override fun createSqlFormatter(database: Database, beautifySql: Boolean, indentSize: Int) =
|
||||
CustomSqlFormatter(database, beautifySql, indentSize)
|
||||
})
|
||||
return Database.connect(
|
||||
dataSource,
|
||||
dialect = object : SqlDialect {
|
||||
override fun createSqlFormatter(database: Database, beautifySql: Boolean, indentSize: Int) =
|
||||
CustomSqlFormatter(database, beautifySql, indentSize)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@Singleton
|
||||
|
||||
@ -11,8 +11,9 @@ internal class VarcharArraySqlType : SqlType<List<String>>(Types.ARRAY, typeName
|
||||
override fun doGetResult(rs: ResultSet, index: Int): List<String>? {
|
||||
return when (val array = rs.getObject(index)) {
|
||||
null -> null
|
||||
is java.sql.Array -> (array.array as Array<*>).filterNotNull().map { it.toString() }
|
||||
is Array<*> -> array.map { it.toString() }
|
||||
else -> error("")
|
||||
else -> error("Unable to deserialize varchar[]")
|
||||
}
|
||||
}
|
||||
|
||||
@ -27,7 +28,7 @@ data class ArrayContainsExpression(
|
||||
val left: ScalarExpression<*>,
|
||||
val right: ScalarExpression<*>,
|
||||
override val sqlType: SqlType<Boolean> = BooleanSqlType,
|
||||
override val isLeafNode: Boolean = false
|
||||
override val isLeafNode: Boolean = false,
|
||||
) : ScalarExpression<Boolean>() {
|
||||
override val extraProperties: Map<String, Any> get() = emptyMap()
|
||||
}
|
||||
|
||||
@ -13,7 +13,7 @@ interface NoteRepository {
|
||||
limit: Int = 20,
|
||||
offset: Int = 0,
|
||||
tag: String? = null,
|
||||
deleted: Boolean = false
|
||||
deleted: Boolean = false,
|
||||
): List<PersistedNoteMetadata>
|
||||
|
||||
fun count(userId: Int, tag: String? = null, deleted: Boolean = false): Int
|
||||
@ -26,6 +26,8 @@ interface NoteRepository {
|
||||
fun create(userId: Int, note: Note): PersistedNote
|
||||
fun find(userId: Int, uuid: UUID): PersistedNote?
|
||||
fun update(userId: Int, uuid: UUID, note: Note): PersistedNote?
|
||||
fun import(userId: Int, notes: List<ExportedNote>)
|
||||
|
||||
fun export(userId: Int): List<ExportedNote>
|
||||
fun findAllDetails(userId: Int): List<PersistedNote>
|
||||
|
||||
|
||||
@ -3,6 +3,7 @@ package be.simplenotes.persistence.repositories
|
||||
import be.simplenotes.persistence.*
|
||||
import be.simplenotes.persistence.converters.NoteConverter
|
||||
import be.simplenotes.persistence.extensions.arrayContains
|
||||
import be.simplenotes.types.ExportedNote
|
||||
import be.simplenotes.types.Note
|
||||
import be.simplenotes.types.PersistedNote
|
||||
import org.ktorm.database.Database
|
||||
@ -10,7 +11,7 @@ import org.ktorm.dsl.*
|
||||
import org.ktorm.entity.*
|
||||
import java.time.LocalDateTime
|
||||
import java.util.*
|
||||
import javax.inject.Singleton
|
||||
import jakarta.inject.Singleton
|
||||
|
||||
@Singleton
|
||||
internal class NoteRepositoryImpl(
|
||||
@ -40,14 +41,18 @@ internal class NoteRepositoryImpl(
|
||||
val uuid = UUID.randomUUID()
|
||||
val entity = converter.toEntity(note, uuid, userId, LocalDateTime.now())
|
||||
db.notes.add(entity)
|
||||
db.batchInsert(Tags) {
|
||||
note.tags.forEach { tagName ->
|
||||
item {
|
||||
set(it.noteUuid, uuid)
|
||||
set(it.name, tagName)
|
||||
|
||||
note.tags.takeIf { it.isNotEmpty() }?.run {
|
||||
db.batchInsert(Tags) {
|
||||
forEach { tagName ->
|
||||
item {
|
||||
set(it.noteUuid, uuid)
|
||||
set(it.name, tagName)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return find(userId, uuid) ?: error("Note not found")
|
||||
}
|
||||
|
||||
@ -114,6 +119,43 @@ internal class NoteRepositoryImpl(
|
||||
.map { it.getInt(1) }
|
||||
.first()
|
||||
|
||||
override fun import(userId: Int, notes: List<ExportedNote>) {
|
||||
if (notes.isEmpty()) return
|
||||
val notesByID = notes.associateBy { UUID.randomUUID() }
|
||||
val tags = sequence<Pair<UUID, String>> {
|
||||
notesByID.entries.forEach { (key, value) ->
|
||||
value.tags.forEach { tag ->
|
||||
yield(key to tag)
|
||||
}
|
||||
}
|
||||
}.toList()
|
||||
|
||||
db.batchInsert(Notes) {
|
||||
notesByID.forEach { (uuid, note) ->
|
||||
item {
|
||||
set(it.uuid, uuid)
|
||||
set(it.userId, userId)
|
||||
set(it.title, note.title)
|
||||
set(it.html, note.html)
|
||||
set(it.markdown, note.markdown)
|
||||
set(it.updatedAt, note.updatedAt)
|
||||
set(it.deleted, note.trash)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tags.takeIf { it.isNotEmpty() }?.run {
|
||||
db.batchInsert(Tags) {
|
||||
forEach { (uuid, tagName) ->
|
||||
item {
|
||||
set(it.noteUuid, uuid)
|
||||
set(it.name, tagName)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun export(userId: Int) = db.noteWithTags
|
||||
.filterColumns { it.columns - it.userId - it.public }
|
||||
.filter { it.userId eq userId }
|
||||
|
||||
@ -10,7 +10,7 @@ import org.ktorm.dsl.*
|
||||
import org.ktorm.entity.any
|
||||
import org.ktorm.entity.find
|
||||
import java.sql.SQLIntegrityConstraintViolationException
|
||||
import javax.inject.Singleton
|
||||
import jakarta.inject.Singleton
|
||||
|
||||
@Singleton
|
||||
internal class UserRepositoryImpl(
|
||||
|
||||
@ -11,7 +11,7 @@ import javax.sql.DataSource
|
||||
@ResourceLock("h2")
|
||||
abstract class DbTest {
|
||||
|
||||
val beanContext = ApplicationContext.build().deduceEnvironment(false).environments("test").start()
|
||||
val beanContext = ApplicationContext.builder().deduceEnvironment(false).environments("test").start()
|
||||
|
||||
inline fun <reified T> BeanContext.getBean(): T = getBean(T::class.java)
|
||||
|
||||
@ -22,6 +22,8 @@ abstract class DbTest {
|
||||
|
||||
Flyway.configure()
|
||||
.dataSource(dataSource)
|
||||
.loggers("slf4j")
|
||||
.cleanDisabled(false)
|
||||
.load()
|
||||
.clean()
|
||||
|
||||
|
||||
@ -13,7 +13,6 @@ import org.junit.jupiter.api.BeforeEach
|
||||
import org.junit.jupiter.api.DisplayName
|
||||
import org.junit.jupiter.api.Nested
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.parallel.ResourceLock
|
||||
import org.ktorm.database.Database
|
||||
import org.ktorm.dsl.eq
|
||||
import org.ktorm.entity.filter
|
||||
@ -22,7 +21,6 @@ import org.ktorm.entity.mapColumns
|
||||
import org.ktorm.entity.toList
|
||||
import java.sql.SQLIntegrityConstraintViolationException
|
||||
|
||||
|
||||
internal class NoteRepositoryImplTest : DbTest() {
|
||||
|
||||
private lateinit var noteRepo: NoteRepository
|
||||
@ -86,14 +84,14 @@ internal class NoteRepositoryImplTest : DbTest() {
|
||||
.hasSize(3)
|
||||
.usingElementComparatorIgnoringFields("updatedAt")
|
||||
.containsExactlyInAnyOrderElementsOf(
|
||||
notes1.map { it.toPersistedMeta() }
|
||||
notes1.map { it.toPersistedMeta() },
|
||||
)
|
||||
|
||||
assertThat(noteRepo.findAll(user2.id))
|
||||
.hasSize(1)
|
||||
.usingElementComparatorIgnoringFields("updatedAt")
|
||||
.containsExactlyInAnyOrderElementsOf(
|
||||
notes2.map { it.toPersistedMeta() }
|
||||
notes2.map { it.toPersistedMeta() },
|
||||
)
|
||||
|
||||
assertThat(noteRepo.findAll(1000)).isEmpty()
|
||||
@ -131,7 +129,9 @@ internal class NoteRepositoryImplTest : DbTest() {
|
||||
|
||||
val note = db.notes.find { Notes.title eq fakeNote.title }!!
|
||||
.let { entity ->
|
||||
val tags = db.tags.filter { be.simplenotes.persistence.Tags.noteUuid eq entity.uuid }.mapColumns { be.simplenotes.persistence.Tags.name } as List<String>
|
||||
val tags = db.tags.filter {
|
||||
be.simplenotes.persistence.Tags.noteUuid eq entity.uuid
|
||||
}.mapColumns { be.simplenotes.persistence.Tags.name } as List<String>
|
||||
PersistedNote(
|
||||
uuid = entity.uuid,
|
||||
title = entity.title,
|
||||
|
||||
@ -22,5 +22,5 @@ internal fun Document.toNoteMeta() = PersistedNoteMetadata(
|
||||
title = get(titleField),
|
||||
uuid = UuidFieldConverter.fromDoc(get(uuidField)),
|
||||
updatedAt = LocalDateTimeFieldConverter.fromDoc(get(updatedAtField)),
|
||||
tags = TagsFieldConverter.fromDoc(get(tagsField))
|
||||
tags = TagsFieldConverter.fromDoc(get(tagsField)),
|
||||
)
|
||||
|
||||
@ -12,8 +12,8 @@ import org.slf4j.LoggerFactory
|
||||
import java.io.File
|
||||
import java.nio.file.Path
|
||||
import java.util.*
|
||||
import javax.inject.Named
|
||||
import javax.inject.Singleton
|
||||
import jakarta.inject.Named
|
||||
import jakarta.inject.Singleton
|
||||
|
||||
@Singleton
|
||||
internal class NoteSearcherImpl(@Named("search-index") basePath: Path) : NoteSearcher {
|
||||
|
||||
@ -4,7 +4,7 @@ import be.simplenotes.config.DataConfig
|
||||
import io.micronaut.context.annotation.Factory
|
||||
import io.micronaut.context.annotation.Prototype
|
||||
import java.nio.file.Path
|
||||
import javax.inject.Named
|
||||
import jakarta.inject.Named
|
||||
|
||||
@Factory
|
||||
class SearchModule {
|
||||
|
||||
@ -31,7 +31,7 @@ internal class NoteSearcherImplTest {
|
||||
html = "",
|
||||
updatedAt = LocalDateTime.MIN,
|
||||
uuid = uuid,
|
||||
public = false
|
||||
public = false,
|
||||
)
|
||||
searcher.indexNote(1, note)
|
||||
return note
|
||||
|
||||
@ -8,5 +8,3 @@ include(":types")
|
||||
include(":persistence")
|
||||
include(":css")
|
||||
include(":junit-config")
|
||||
|
||||
enableFeaturePreview("VERSION_CATALOGS")
|
||||
|
||||
@ -3,15 +3,15 @@ package be.simplenotes.views
|
||||
import be.simplenotes.types.LoggedInUser
|
||||
import kotlinx.html.*
|
||||
import kotlinx.html.ThScope.col
|
||||
import javax.inject.Named
|
||||
import javax.inject.Singleton
|
||||
import jakarta.inject.Named
|
||||
import jakarta.inject.Singleton
|
||||
|
||||
@Singleton
|
||||
class BaseView(@Named("styles") styles: String) : View(styles) {
|
||||
fun renderHome(loggedInUser: LoggedInUser?) = renderPage(
|
||||
title = "Home",
|
||||
description = "A fast and simple note taking website",
|
||||
loggedInUser = loggedInUser
|
||||
loggedInUser = loggedInUser,
|
||||
) {
|
||||
section("text-center my-2 p-2") {
|
||||
h1("text-5xl casual") {
|
||||
@ -21,7 +21,6 @@ class BaseView(@Named("styles") styles: String) : View(styles) {
|
||||
}
|
||||
|
||||
div("container mx-auto flex flex-wrap justify-center content-center") {
|
||||
|
||||
div("md:order-1 order-2 flipped p-4 my-10 w-full md:w-1/2") {
|
||||
attributes["aria-label"] = "demo"
|
||||
div("flex justify-between mb-4") {
|
||||
|
||||
@ -4,8 +4,8 @@ import be.simplenotes.views.components.Alert
|
||||
import be.simplenotes.views.components.alert
|
||||
import kotlinx.html.a
|
||||
import kotlinx.html.div
|
||||
import javax.inject.Named
|
||||
import javax.inject.Singleton
|
||||
import jakarta.inject.Named
|
||||
import jakarta.inject.Singleton
|
||||
|
||||
@Singleton
|
||||
class ErrorView(@Named("styles") styles: String) : View(styles) {
|
||||
@ -23,7 +23,7 @@ class ErrorView(@Named("styles") styles: String) : View(styles) {
|
||||
Alert.Warning,
|
||||
errorType.title,
|
||||
"Please try again later",
|
||||
multiline = true
|
||||
multiline = true,
|
||||
)
|
||||
Type.NotFound -> alert(Alert.Warning, errorType.title, "Page not found", multiline = true)
|
||||
Type.Other -> alert(Alert.Warning, errorType.title)
|
||||
|
||||
@ -6,8 +6,8 @@ import be.simplenotes.types.PersistedNoteMetadata
|
||||
import be.simplenotes.views.components.*
|
||||
import io.konform.validation.ValidationError
|
||||
import kotlinx.html.*
|
||||
import javax.inject.Named
|
||||
import javax.inject.Singleton
|
||||
import jakarta.inject.Named
|
||||
import jakarta.inject.Singleton
|
||||
|
||||
@Singleton
|
||||
class NoteView(@Named("styles") styles: String) : View(styles) {
|
||||
@ -41,7 +41,7 @@ class NoteView(@Named("styles") styles: String) : View(styles) {
|
||||
|---
|
||||
|
|
||||
""".trimMargin(
|
||||
"|"
|
||||
"|",
|
||||
)
|
||||
}
|
||||
submitButton("Save")
|
||||
@ -123,10 +123,9 @@ class NoteView(@Named("styles") styles: String) : View(styles) {
|
||||
fun renderedNote(loggedInUser: LoggedInUser?, note: PersistedNote, shared: Boolean) = renderPage(
|
||||
note.title,
|
||||
loggedInUser = loggedInUser,
|
||||
scripts = listOf("/highlight.10.1.2.js", "/init-highlight.0.0.1.js")
|
||||
scripts = listOf("/highlight.10.1.2.js", "/init-highlight.0.0.1.js"),
|
||||
) {
|
||||
div("container mx-auto p-4") {
|
||||
|
||||
if (shared) {
|
||||
p("p-4 bg-gray-800") {
|
||||
+"You are viewing a public note "
|
||||
@ -172,14 +171,14 @@ class NoteView(@Named("styles") styles: String) : View(styles) {
|
||||
form(method = FormMethod.post, classes = "inline flex space-x-2 justify-end mb-4") {
|
||||
a(
|
||||
href = "/notes/${note.uuid}/edit",
|
||||
classes = "btn btn-green"
|
||||
classes = "btn btn-green",
|
||||
) { +"Edit" }
|
||||
span {
|
||||
button(
|
||||
type = ButtonType.submit,
|
||||
name = if (note.public) "private" else "public",
|
||||
classes = "font-semibold border-b-4 ${if (note.public) "border-teal-200" else "border-green-500"}" +
|
||||
" p-2 rounded-l bg-teal-200 text-gray-800"
|
||||
" p-2 rounded-l bg-teal-200 text-gray-800",
|
||||
) {
|
||||
+"Private"
|
||||
}
|
||||
@ -188,7 +187,7 @@ class NoteView(@Named("styles") styles: String) : View(styles) {
|
||||
name = if (note.public) "private" else "public",
|
||||
classes = "font-semibold border-b-4 " +
|
||||
(if (!note.public) "border-teal-200" else "border-green-500") +
|
||||
" p-2 rounded-r bg-teal-200 text-gray-800"
|
||||
" p-2 rounded-r bg-teal-200 text-gray-800",
|
||||
) {
|
||||
+"Public"
|
||||
}
|
||||
@ -196,7 +195,7 @@ class NoteView(@Named("styles") styles: String) : View(styles) {
|
||||
button(
|
||||
type = ButtonType.submit,
|
||||
name = "delete",
|
||||
classes = "btn btn-red"
|
||||
classes = "btn btn-red",
|
||||
) { +"Delete" }
|
||||
}
|
||||
}
|
||||
|
||||
@ -6,10 +6,13 @@ import be.simplenotes.views.components.alert
|
||||
import be.simplenotes.views.components.input
|
||||
import be.simplenotes.views.extensions.summary
|
||||
import io.konform.validation.ValidationError
|
||||
import jakarta.inject.Named
|
||||
import jakarta.inject.Singleton
|
||||
import kotlinx.html.*
|
||||
import kotlinx.html.ButtonType.submit
|
||||
import javax.inject.Named
|
||||
import javax.inject.Singleton
|
||||
import kotlinx.html.FormEncType.multipartFormData
|
||||
import kotlinx.html.FormMethod.post
|
||||
import kotlinx.html.InputType.file
|
||||
|
||||
@Singleton
|
||||
class SettingView(@Named("styles") styles: String) : View(styles) {
|
||||
@ -20,7 +23,6 @@ class SettingView(@Named("styles") styles: String) : View(styles) {
|
||||
validationErrors: List<ValidationError> = emptyList(),
|
||||
) = renderPage("Settings", loggedInUser = loggedInUser) {
|
||||
div("container mx-auto") {
|
||||
|
||||
section("m-4 p-4 bg-gray-800 rounded") {
|
||||
h1("text-xl") {
|
||||
+"Welcome "
|
||||
@ -29,36 +31,54 @@ class SettingView(@Named("styles") styles: String) : View(styles) {
|
||||
}
|
||||
|
||||
section("m-4 p-2 bg-gray-800 rounded flex flex-wrap justify-around items-end") {
|
||||
|
||||
form(classes = "m-2", method = FormMethod.post, action = "/export") {
|
||||
form(classes = "m-2 flex-1", method = post, action = "/export") {
|
||||
button(
|
||||
name = "display",
|
||||
classes = "inline btn btn-teal block",
|
||||
type = submit
|
||||
classes = "btn btn-teal block",
|
||||
type = submit,
|
||||
) { +"Display my data" }
|
||||
}
|
||||
|
||||
form(classes = "m-2", method = FormMethod.post, action = "/export") {
|
||||
|
||||
form(classes = "m-2 flex-1", method = post, action = "/export") {
|
||||
div {
|
||||
listOf("json", "zip").forEach { format ->
|
||||
radioInput(name = "format") {
|
||||
id = format
|
||||
attributes["value"] = format
|
||||
if (format == "json") attributes["checked"] = ""
|
||||
else attributes["class"] = "ml-4"
|
||||
}
|
||||
label(classes = "ml-2") {
|
||||
attributes["for"] = format
|
||||
+format
|
||||
div("px-2") {
|
||||
radioInput(
|
||||
name = "format",
|
||||
classes =
|
||||
"checked:bg-blue-500 bg-gray-200 appearance-none rounded-full border-2 h-5 w-5",
|
||||
) {
|
||||
id = format
|
||||
attributes["value"] = format
|
||||
if (format == "json") attributes["checked"] = ""
|
||||
}
|
||||
label(classes = "ml-2") {
|
||||
attributes["for"] = format
|
||||
+format
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
button(name = "download", classes = "inline btn btn-green block mt-2", type = submit) {
|
||||
button(name = "download", classes = "btn btn-green block mt-2", type = submit) {
|
||||
+"Download my data"
|
||||
}
|
||||
}
|
||||
|
||||
form(classes = "m-2 flex-1", method = post, encType = multipartFormData, action = "/import") {
|
||||
input(
|
||||
file,
|
||||
classes = "file:hidden mb-4",
|
||||
name = "file",
|
||||
) {
|
||||
attributes["accept"] = ".json,application/json"
|
||||
}
|
||||
button(
|
||||
name = "import",
|
||||
classes = "btn btn-teal block",
|
||||
type = submit,
|
||||
) { +"Import" }
|
||||
}
|
||||
}
|
||||
|
||||
section(classes = "m-4 p-4 bg-gray-800 rounded") {
|
||||
@ -69,7 +89,6 @@ class SettingView(@Named("styles") styles: String) : View(styles) {
|
||||
error?.let { alert(Alert.Warning, error) }
|
||||
|
||||
details {
|
||||
|
||||
if (error != null || validationErrors.isNotEmpty()) {
|
||||
attributes["open"] = ""
|
||||
}
|
||||
@ -81,13 +100,13 @@ class SettingView(@Named("styles") styles: String) : View(styles) {
|
||||
}
|
||||
}
|
||||
|
||||
form(classes = "mt-4", method = FormMethod.post) {
|
||||
form(classes = "mt-4", method = post) {
|
||||
input(
|
||||
id = "password",
|
||||
placeholder = "Password",
|
||||
autoComplete = "off",
|
||||
type = InputType.password,
|
||||
error = validationErrors.find { it.dataPath == ".password" }?.message
|
||||
error = validationErrors.find { it.dataPath == ".password" }?.message,
|
||||
)
|
||||
checkBoxInput(name = "checked") {
|
||||
id = "checked"
|
||||
@ -100,7 +119,7 @@ class SettingView(@Named("styles") styles: String) : View(styles) {
|
||||
button(
|
||||
type = submit,
|
||||
classes = "block mt-4 btn btn-red",
|
||||
name = "delete"
|
||||
name = "delete",
|
||||
) { +"I'm sure" }
|
||||
}
|
||||
}
|
||||
|
||||
@ -7,8 +7,8 @@ import be.simplenotes.views.components.input
|
||||
import be.simplenotes.views.components.submitButton
|
||||
import io.konform.validation.ValidationError
|
||||
import kotlinx.html.*
|
||||
import javax.inject.Named
|
||||
import javax.inject.Singleton
|
||||
import jakarta.inject.Named
|
||||
import jakarta.inject.Singleton
|
||||
|
||||
@Singleton
|
||||
class UserView(@Named("styles") styles: String) : View(styles) {
|
||||
@ -23,7 +23,7 @@ class UserView(@Named("styles") styles: String) : View(styles) {
|
||||
error,
|
||||
validationErrors,
|
||||
"Create an account",
|
||||
"Register"
|
||||
"Register",
|
||||
) {
|
||||
+"Already have an account? "
|
||||
a(href = "/login", classes = "no-underline text-blue-500 hover:text-blue-400 font-bold") { +"Sign In" }
|
||||
@ -63,14 +63,14 @@ class UserView(@Named("styles") styles: String) : View(styles) {
|
||||
id = "username",
|
||||
placeholder = "Username",
|
||||
autoComplete = "username",
|
||||
error = validationErrors.find { it.dataPath == ".username" }?.message
|
||||
error = validationErrors.find { it.dataPath == ".username" }?.message,
|
||||
)
|
||||
input(
|
||||
id = "password",
|
||||
placeholder = "Password",
|
||||
autoComplete = "new-password",
|
||||
type = InputType.password,
|
||||
error = validationErrors.find { it.dataPath == ".password" }?.message
|
||||
error = validationErrors.find { it.dataPath == ".password" }?.message,
|
||||
)
|
||||
submitButton(submit)
|
||||
}
|
||||
|
||||
@ -8,13 +8,13 @@ internal fun FlowContent.input(
|
||||
placeholder: String,
|
||||
id: String,
|
||||
autoComplete: String? = null,
|
||||
error: String? = null
|
||||
error: String? = null,
|
||||
) {
|
||||
val colors = "bg-gray-800 border-gray-700 focus:border-teal-500 text-white"
|
||||
div("mb-8") {
|
||||
input(
|
||||
type = type,
|
||||
classes = "$colors rounded w-full border appearance-none focus:outline-none text-base p-2"
|
||||
classes = "$colors rounded w-full border appearance-none focus:outline-none text-base p-2",
|
||||
) {
|
||||
attributes["placeholder"] = placeholder
|
||||
attributes["aria-label"] = placeholder
|
||||
@ -30,7 +30,7 @@ internal fun FlowContent.submitButton(text: String) {
|
||||
div("flex items-center mt-6") {
|
||||
button(
|
||||
type = submit,
|
||||
classes = "btn btn-teal w-full"
|
||||
classes = "btn btn-teal w-full",
|
||||
) { +text }
|
||||
}
|
||||
}
|
||||
|
||||
@ -10,11 +10,11 @@ internal fun DIV.noteListHeader(numberOfDeletedNotes: Int, query: String = "") {
|
||||
span {
|
||||
a(
|
||||
href = "/notes/trash",
|
||||
classes = "btn btn-teal"
|
||||
classes = "btn btn-teal",
|
||||
) { +"Trash ($numberOfDeletedNotes)" }
|
||||
a(
|
||||
href = "/notes/new",
|
||||
classes = "ml-2 btn btn-green"
|
||||
classes = "ml-2 btn btn-green",
|
||||
) { +"New" }
|
||||
}
|
||||
}
|
||||
|
||||
@ -8,7 +8,7 @@ internal class SUMMARY(consumer: TagConsumer<*>) :
|
||||
consumer,
|
||||
emptyMap(),
|
||||
inlineTag = true,
|
||||
emptyTag = false
|
||||
emptyTag = false,
|
||||
),
|
||||
HtmlInlineTag
|
||||
|
||||
|
||||
@ -8,5 +8,5 @@ import java.util.*
|
||||
private val prettyTime = PrettyTime()
|
||||
|
||||
internal fun LocalDateTime.toTimeAgo(): String = prettyTime.format(
|
||||
Date.from(atZone(ZoneId.systemDefault()).toInstant())
|
||||
Date.from(atZone(ZoneId.systemDefault()).toInstant()),
|
||||
)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user