2 Commits

Author SHA1 Message Date
hubert 94b61f3de5 We don't need that much RAM 2020-09-30 18:31:17 +02:00
hubert dcc70aab3a Unload bootstraping modules after startup 2020-09-30 18:30:19 +02:00
153 changed files with 791 additions and 1482 deletions
+2 -2
View File
@@ -125,8 +125,8 @@ data/
letsencrypt/ letsencrypt/
# generated resources # generated resources
simplenotes-app/src/main/resources/css-manifest.json app/src/main/resources/css-manifest.json
simplenotes-app/src/main/resources/static/styles* app/src/main/resources/static/styles*
# h2 db # h2 db
*.db *.db
+12 -18
View File
@@ -4,23 +4,19 @@ WORKDIR /tmp
# Cache dependencies # Cache dependencies
COPY pom.xml . COPY pom.xml .
COPY simplenotes-test-resources/pom.xml simplenotes-test-resources/pom.xml COPY app/pom.xml app/pom.xml
COPY simplenotes-types/pom.xml simplenotes-types/pom.xml COPY domain/pom.xml domain/pom.xml
COPY simplenotes-config/pom.xml simplenotes-config/pom.xml COPY persistance/pom.xml persistance/pom.xml
COPY simplenotes-persistance/pom.xml simplenotes-persistance/pom.xml COPY shared/pom.xml shared/pom.xml
COPY simplenotes-search/pom.xml simplenotes-search/pom.xml COPY search/pom.xml search/pom.xml
COPY simplenotes-domain/pom.xml simplenotes-domain/pom.xml
COPY simplenotes-app/pom.xml simplenotes-app/pom.xml
RUN mvn verify clean --fail-never RUN mvn verify clean --fail-never
COPY simplenotes-test-resources/src simplenotes-test-resources/src COPY app/src app/src
COPY simplenotes-types/src simplenotes-types/src COPY domain/src domain/src
COPY simplenotes-config/src simplenotes-config/src COPY persistance/src persistance/src
COPY simplenotes-persistance/src simplenotes-persistance/src COPY shared/src shared/src
COPY simplenotes-search/src simplenotes-search/src COPY search/src search/src
COPY simplenotes-domain/src simplenotes-domain/src
COPY simplenotes-app/src simplenotes-app/src
RUN mvn -Dstyle.color=always package RUN mvn -Dstyle.color=always package
@@ -36,8 +32,6 @@ RUN strip -p --strip-unneeded /myjdk/lib/server/libjvm.so
FROM alpine FROM alpine
RUN apk add --no-cache curl
ENV APPLICATION_USER simplenotes ENV APPLICATION_USER simplenotes
RUN adduser -D -g '' $APPLICATION_USER RUN adduser -D -g '' $APPLICATION_USER
@@ -46,8 +40,8 @@ RUN chown -R $APPLICATION_USER /app
USER $APPLICATION_USER USER $APPLICATION_USER
COPY --from=builder /tmp/simplenotes-app/target/simplenotes-app-*.jar /app/simplenotes.jar COPY --from=builder /tmp/app/target/app-*.jar /app/app.jar
COPY --from=jdkbuilder /myjdk /myjdk COPY --from=jdkbuilder /myjdk /myjdk
WORKDIR /app WORKDIR /app
CMD ["/myjdk/bin/java", "-server", "-XX:+UnlockExperimentalVMOptions", "-Xms64m", "-Xmx256m", "-XX:+UseG1GC", "-XX:MaxGCPauseMillis=100", "-XX:+UseStringDeduplication", "-jar", "simplenotes.jar"] CMD ["/myjdk/bin/java", "-server", "-XX:+UnlockExperimentalVMOptions", "-Xms64m", "-Xmx256m", "-XX:+UseG1GC", "-XX:MaxGCPauseMillis=100", "-XX:+UseStringDeduplication", "-jar", "app.jar"]
+14 -66
View File
@@ -1,70 +1,48 @@
<?xml version="1.0" encoding="UTF-8"?> <project>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent> <parent>
<artifactId>simplenotes-parent</artifactId> <artifactId>parent</artifactId>
<groupId>be.simplenotes</groupId> <groupId>be.simplenotes</groupId>
<version>1.0-SNAPSHOT</version> <version>1.0-SNAPSHOT</version>
</parent> </parent>
<modelVersion>4.0.0</modelVersion> <modelVersion>4.0.0</modelVersion>
<artifactId>simplenotes-app</artifactId> <artifactId>app</artifactId>
<properties> <properties>
<http4k.version>3.268.0</http4k.version> <http4k.version>3.258.0</http4k.version>
</properties> </properties>
<dependencies> <dependencies>
<dependency> <dependency>
<groupId>be.simplenotes</groupId> <groupId>be.simplenotes</groupId>
<artifactId>simplenotes-persistance</artifactId> <artifactId>persistance</artifactId>
<version>1.0-SNAPSHOT</version> <version>1.0-SNAPSHOT</version>
</dependency> </dependency>
<dependency> <dependency>
<groupId>be.simplenotes</groupId> <groupId>be.simplenotes</groupId>
<artifactId>simplenotes-search</artifactId> <artifactId>search</artifactId>
<version>1.0-SNAPSHOT</version> <version>1.0-SNAPSHOT</version>
</dependency> </dependency>
<dependency> <dependency>
<groupId>be.simplenotes</groupId> <groupId>be.simplenotes</groupId>
<artifactId>simplenotes-domain</artifactId> <artifactId>domain</artifactId>
<version>1.0-SNAPSHOT</version> <version>1.0-SNAPSHOT</version>
</dependency> </dependency>
<dependency> <dependency>
<groupId>be.simplenotes</groupId> <groupId>be.simplenotes</groupId>
<artifactId>simplenotes-config</artifactId> <artifactId>shared</artifactId>
<version>1.0-SNAPSHOT</version> <version>1.0-SNAPSHOT</version>
</dependency> </dependency>
<dependency> <dependency>
<groupId>org.http4k</groupId> <groupId>org.http4k</groupId>
<artifactId>http4k-core</artifactId> <artifactId>http4k-core</artifactId>
<version>${http4k.version}</version>
</dependency> </dependency>
<!--
<dependency> <dependency>
<groupId>org.http4k</groupId> <groupId>org.http4k</groupId>
<artifactId>http4k-server-jetty</artifactId> <artifactId>http4k-server-jetty</artifactId>
<version>${http4k.version}</version>
</dependency> </dependency>
-->
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-server</artifactId>
<version>9.4.32.v20200930</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-servlet</artifactId>
<version>9.4.32.v20200930</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>4.0.1</version>
<scope>compile</scope>
</dependency>
<dependency> <dependency>
<groupId>org.jetbrains.kotlinx</groupId> <groupId>org.jetbrains.kotlinx</groupId>
<artifactId>kotlinx-html-jvm</artifactId> <artifactId>kotlinx-html-jvm</artifactId>
@@ -72,7 +50,7 @@
</dependency> </dependency>
<dependency> <dependency>
<groupId>org.jetbrains.kotlinx</groupId> <groupId>org.jetbrains.kotlinx</groupId>
<artifactId>kotlinx-serialization-json-jvm</artifactId> <artifactId>kotlinx-serialization-runtime</artifactId>
</dependency> </dependency>
<dependency> <dependency>
<groupId>org.ocpsoft.prettytime</groupId> <groupId>org.ocpsoft.prettytime</groupId>
@@ -80,24 +58,9 @@
<version>4.0.5.Final</version> <version>4.0.5.Final</version>
</dependency> </dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
</dependency>
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
<dependency> <dependency>
<groupId>be.simplenotes</groupId> <groupId>be.simplenotes</groupId>
<artifactId>simplenotes-test-resources</artifactId> <artifactId>shared</artifactId>
<version>1.0-SNAPSHOT</version> <version>1.0-SNAPSHOT</version>
<type>test-jar</type> <type>test-jar</type>
<scope>test</scope> <scope>test</scope>
@@ -105,32 +68,17 @@
<dependency> <dependency>
<groupId>org.http4k</groupId> <groupId>org.http4k</groupId>
<artifactId>http4k-testing-hamkrest</artifactId> <artifactId>http4k-testing-hamkrest</artifactId>
<version>${http4k.version}</version>
<scope>test</scope> <scope>test</scope>
</dependency> </dependency>
<dependency>
<groupId>me.liuwj.ktorm</groupId>
<artifactId>ktorm-core</artifactId>
</dependency>
</dependencies> </dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.http4k</groupId>
<artifactId>http4k-bom</artifactId>
<version>${http4k.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<build> <build>
<plugins> <plugins>
<plugin> <plugin>
<groupId>org.apache.maven.plugins</groupId> <groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId> <artifactId>maven-shade-plugin</artifactId>
<version>3.2.4</version>
<executions> <executions>
<execution> <execution>
<phase>package</phase> <phase>package</phase>
@@ -1,8 +1,8 @@
package be.simplenotes.app package be.simplenotes.app
import be.simplenotes.config.DataSourceConfig import be.simplenotes.shared.config.DataSourceConfig
import be.simplenotes.config.JwtConfig import be.simplenotes.shared.config.JwtConfig
import be.simplenotes.config.ServerConfig import be.simplenotes.shared.config.ServerConfig
import java.util.* import java.util.*
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
@@ -2,7 +2,7 @@ package be.simplenotes.app
import org.http4k.server.Http4kServer import org.http4k.server.Http4kServer
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import be.simplenotes.config.ServerConfig as SimpleNotesServerConfig import be.simplenotes.shared.config.ServerConfig as SimpleNotesServerConfig
class Server( class Server(
private val config: SimpleNotesServerConfig, private val config: SimpleNotesServerConfig,
@@ -1,46 +1,55 @@
package be.simplenotes.app.api package be.simplenotes.app.api
import be.simplenotes.app.extensions.auto import be.simplenotes.app.extensions.json
import be.simplenotes.app.utils.parseSearchTerms import be.simplenotes.app.utils.parseSearchTerms
import be.simplenotes.types.PersistedNote import be.simplenotes.domain.model.PersistedNote
import be.simplenotes.types.PersistedNoteMetadata import be.simplenotes.domain.model.PersistedNoteMetadata
import be.simplenotes.domain.security.JwtPayload import be.simplenotes.domain.security.JwtPayload
import be.simplenotes.domain.usecases.NoteService import be.simplenotes.domain.usecases.NoteService
import kotlinx.serialization.Contextual import kotlinx.serialization.Contextual
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import kotlinx.serialization.builtins.ListSerializer
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import org.http4k.core.Request import org.http4k.core.Request
import org.http4k.core.Response import org.http4k.core.Response
import org.http4k.core.Status.Companion.BAD_REQUEST import org.http4k.core.Status.Companion.BAD_REQUEST
import org.http4k.core.Status.Companion.NOT_FOUND import org.http4k.core.Status.Companion.NOT_FOUND
import org.http4k.core.Status.Companion.OK import org.http4k.core.Status.Companion.OK
import org.http4k.lens.Path import org.http4k.routing.path
import org.http4k.lens.uuid
import java.util.* import java.util.*
class ApiNoteController(private val noteService: NoteService, private val json: Json) { class ApiNoteController(private val noteService: NoteService, private val json: Json) {
fun createNote(request: Request, jwtPayload: JwtPayload): Response { fun createNote(request: Request, jwtPayload: JwtPayload): Response {
val content = noteContentLens(request) val content = json.decodeFromString(NoteContent.serializer(), request.bodyString()).content
return noteService.create(jwtPayload.userId, content).fold( return noteService.create(jwtPayload.userId, content).fold(
{ Response(BAD_REQUEST) }, {
{ uuidContentLens(UuidContent(it.uuid), Response(OK)) } Response(BAD_REQUEST)
},
{
Response(OK).json(json.encodeToString(UuidContent.serializer(), UuidContent(it.uuid)))
}
) )
} }
fun notes(request: Request, jwtPayload: JwtPayload): Response { fun notes(request: Request, jwtPayload: JwtPayload): Response {
val notes = noteService.paginatedNotes(jwtPayload.userId, page = 1).notes val notes = noteService.paginatedNotes(jwtPayload.userId, page = 1).notes
return persistedNotesMetadataLens(notes, Response(OK)) val json = json.encodeToString(ListSerializer(PersistedNoteMetadata.serializer()), notes)
return Response(OK).json(json)
} }
fun note(request: Request, jwtPayload: JwtPayload): Response = fun note(request: Request, jwtPayload: JwtPayload): Response {
noteService.find(jwtPayload.userId, uuidLens(request)) val uuid = request.path("uuid")!!
?.let { persistedNoteLens(it, Response(OK)) }
return noteService.find(jwtPayload.userId, UUID.fromString(uuid))
?.let { Response(OK).json(json.encodeToString(PersistedNote.serializer(), it)) }
?: Response(NOT_FOUND) ?: Response(NOT_FOUND)
}
fun update(request: Request, jwtPayload: JwtPayload): Response { fun update(request: Request, jwtPayload: JwtPayload): Response {
val content = noteContentLens(request) val uuid = UUID.fromString(request.path("uuid")!!)
return noteService.update(jwtPayload.userId, uuidLens(request), content).fold({ val content = json.decodeFromString(NoteContent.serializer(), request.bodyString()).content
return noteService.update(jwtPayload.userId, uuid, content).fold({
Response(BAD_REQUEST) Response(BAD_REQUEST)
}, { }, {
if (it == null) Response(NOT_FOUND) if (it == null) Response(NOT_FOUND)
@@ -49,19 +58,13 @@ class ApiNoteController(private val noteService: NoteService, private val json:
} }
fun search(request: Request, jwtPayload: JwtPayload): Response { fun search(request: Request, jwtPayload: JwtPayload): Response {
val query = searchContentLens(request) val query = json.decodeFromString(SearchContent.serializer(), request.bodyString()).query
val terms = parseSearchTerms(query) val terms = parseSearchTerms(query)
val notes = noteService.search(jwtPayload.userId, terms) val notes = noteService.search(jwtPayload.userId, terms)
return persistedNotesMetadataLens(notes, Response(OK)) val json = json.encodeToString(ListSerializer(PersistedNoteMetadata.serializer()), notes)
return Response(OK).json(json)
} }
private val uuidContentLens = json.auto<UuidContent>().toLens()
private val noteContentLens = json.auto<NoteContent>().map { it.content }.toLens()
private val searchContentLens = json.auto<SearchContent>().map { it.query }.toLens()
private val persistedNotesMetadataLens = json.auto<List<PersistedNoteMetadata>>().toLens()
private val persistedNoteLens = json.auto<PersistedNote>().toLens()
private val uuidLens = Path.uuid().of("uuid")
} }
@Serializable @Serializable
@@ -0,0 +1,26 @@
package be.simplenotes.app.api
import be.simplenotes.app.extensions.json
import be.simplenotes.domain.usecases.UserService
import be.simplenotes.domain.usecases.users.login.LoginForm
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
import org.http4k.core.Request
import org.http4k.core.Response
import org.http4k.core.Status
class ApiUserController(private val userService: UserService, private val json: Json) {
fun login(request: Request): Response {
val form = json.decodeFromString(LoginForm.serializer(), request.bodyString())
val result = userService.login(form)
return result.fold({
Response(Status.BAD_REQUEST)
}, {
Response(Status.OK).json(json.encodeToString(Token.serializer(), Token(it)))
})
}
}
@Serializable
data class Token(val token: String)
@@ -10,7 +10,7 @@ import be.simplenotes.domain.usecases.users.login.*
import be.simplenotes.domain.usecases.users.register.InvalidRegisterForm import be.simplenotes.domain.usecases.users.register.InvalidRegisterForm
import be.simplenotes.domain.usecases.users.register.RegisterForm import be.simplenotes.domain.usecases.users.register.RegisterForm
import be.simplenotes.domain.usecases.users.register.UserExists import be.simplenotes.domain.usecases.users.register.UserExists
import be.simplenotes.config.JwtConfig import be.simplenotes.shared.config.JwtConfig
import org.http4k.core.Method.GET import org.http4k.core.Method.GET
import org.http4k.core.Request import org.http4k.core.Request
import org.http4k.core.Response import org.http4k.core.Response
@@ -0,0 +1,17 @@
package be.simplenotes.app.extensions
import org.http4k.core.Request
import org.http4k.core.Response
import org.http4k.core.Status.Companion.FOUND
import org.http4k.core.Status.Companion.MOVED_PERMANENTLY
fun Response.html(html: String) = body(html)
.header("Content-Type", "text/html; charset=utf-8")
.header("Cache-Control", "no-cache")
fun Response.json(json: String) = body(json).header("Content-Type", "application/json")
fun Response.Companion.redirect(url: String, permanent: Boolean = false) =
Response(if (permanent) MOVED_PERMANENTLY else FOUND).header("Location", url)
fun Request.isSecure() = header("X-Forwarded-Proto")?.contains("https") ?: false
@@ -19,8 +19,9 @@ class AuthFilter(
private val ctx: RequestContexts, private val ctx: RequestContexts,
private val source: JwtSource = JwtSource.Cookie, private val source: JwtSource = JwtSource.Cookie,
private val redirect: Boolean = true, private val redirect: Boolean = true,
) : Filter { ) {
override fun invoke(next: HttpHandler): HttpHandler = { operator fun invoke() = Filter { next ->
{
val token = when (source) { val token = when (source) {
JwtSource.Header -> it.bearerTokenHeader() JwtSource.Header -> it.bearerTokenHeader()
JwtSource.Cookie -> it.bearerTokenCookie() JwtSource.Cookie -> it.bearerTokenCookie()
@@ -38,6 +39,7 @@ class AuthFilter(
else -> next(it) else -> next(it)
} }
} }
}
} }
fun Request.jwtPayload(ctx: RequestContexts): JwtPayload? = ctx[this][authKey] fun Request.jwtPayload(ctx: RequestContexts): JwtPayload? = ctx[this][authKey]
@@ -0,0 +1,33 @@
package be.simplenotes.app.filters
import be.simplenotes.app.extensions.html
import be.simplenotes.app.views.ErrorView
import org.http4k.core.*
import org.slf4j.LoggerFactory
import java.sql.SQLTransientException
class ErrorFilter(private val errorView: ErrorView) {
private val logger = LoggerFactory.getLogger(javaClass)
operator fun invoke(): Filter = Filter { next ->
{
try {
val response = next(it)
if (response.status == Status.NOT_FOUND) Response(Status.NOT_FOUND)
.html(errorView.error(ErrorView.Type.NotFound))
else response
} catch (e: Exception) {
logger.error(e.stackTraceToString())
if (e is SQLTransientException)
Response(Status.SERVICE_UNAVAILABLE).html(errorView.error(ErrorView.Type.SqlTransientError))
.noCache()
else
Response(Status.INTERNAL_SERVER_ERROR).html(errorView.error(ErrorView.Type.Other)).noCache()
} catch (e: NotImplementedError) {
logger.error(e.stackTraceToString())
Response(Status.NOT_IMPLEMENTED).html(errorView.error(ErrorView.Type.Other)).noCache()
}
}
}
}
@@ -0,0 +1,14 @@
package be.simplenotes.app.filters
import org.http4k.core.Filter
import org.http4k.core.HttpHandler
import org.http4k.core.Method
import org.http4k.core.Request
object ImmutableFilter {
operator fun invoke() = Filter { next: HttpHandler ->
{ request: Request ->
next(request).header("Cache-Control", "public, max-age=31536000, immutable")
}
}
}
@@ -0,0 +1,20 @@
package be.simplenotes.app.filters
import org.http4k.core.Filter
import org.http4k.core.HttpHandler
import org.http4k.core.Request
object SecurityFilter {
operator fun invoke() = Filter { next: HttpHandler ->
{ request: Request ->
val response = next(request)
.header("X-Content-Type-Options", "nosniff")
if (response.header("Content-Type")?.contains("text/html") == true) {
response
.header("Content-Security-Policy", "default-src 'self'")
.header("Referrer-Policy", "no-referrer")
} else response
}
}
}
@@ -5,20 +5,19 @@ import be.simplenotes.app.api.ApiUserController
import be.simplenotes.app.filters.AuthFilter import be.simplenotes.app.filters.AuthFilter
import be.simplenotes.app.filters.AuthType import be.simplenotes.app.filters.AuthType
import be.simplenotes.app.filters.JwtSource import be.simplenotes.app.filters.JwtSource
import org.http4k.core.Filter
import org.koin.core.qualifier.named import org.koin.core.qualifier.named
import org.koin.dsl.module import org.koin.dsl.module
val apiModule = module { val apiModule = module {
single { ApiUserController(get(), get()) } single { ApiUserController(get(), get()) }
single { ApiNoteController(get(), get()) } single { ApiNoteController(get(), get()) }
single<Filter>(named("apiAuthFilter")) { single(named("apiAuthFilter")) {
AuthFilter( AuthFilter(
extractor = get(), extractor = get(),
authType = AuthType.Required, authType = AuthType.Required,
ctx = get(), ctx = get(),
source = JwtSource.Header, source = JwtSource.Header,
redirect = false redirect = false
) )()
} }
} }
@@ -2,9 +2,16 @@ package be.simplenotes.app.modules
import be.simplenotes.app.Config import be.simplenotes.app.Config
import org.koin.dsl.module import org.koin.dsl.module
import org.koin.dsl.onClose
val configModule = module { val configModule = module {
single { Config() } single { Config() } onClose {
println("Unloaded config")
println("Unloaded config")
println("Unloaded config")
println("Unloaded config")
}
single { get<Config>().dataSourceConfig } single { get<Config>().dataSourceConfig }
single { get<Config>().jwtConfig } single { get<Config>().jwtConfig }
single { get<Config>().serverConfig } single { get<Config>().serverConfig }
@@ -1,6 +1,9 @@
package be.simplenotes.app.modules package be.simplenotes.app.modules
import be.simplenotes.app.controllers.* import be.simplenotes.app.controllers.BaseController
import be.simplenotes.app.controllers.NoteController
import be.simplenotes.app.controllers.SettingsController
import be.simplenotes.app.controllers.UserController
import be.simplenotes.app.views.BaseView import be.simplenotes.app.views.BaseView
import be.simplenotes.app.views.NoteView import be.simplenotes.app.views.NoteView
import be.simplenotes.app.views.SettingView import be.simplenotes.app.views.SettingView
@@ -13,7 +16,6 @@ val userModule = module {
} }
val baseModule = module { val baseModule = module {
single { HealthCheckController(get()) }
single { BaseController(get()) } single { BaseController(get()) }
single { BaseView(get()) } single { BaseView(get()) }
} }
@@ -4,18 +4,16 @@ import be.simplenotes.app.Server
import be.simplenotes.app.filters.AuthFilter import be.simplenotes.app.filters.AuthFilter
import be.simplenotes.app.filters.AuthType import be.simplenotes.app.filters.AuthType
import be.simplenotes.app.filters.ErrorFilter import be.simplenotes.app.filters.ErrorFilter
import be.simplenotes.app.filters.TransactionFilter
import be.simplenotes.app.jetty.ConnectorBuilder
import be.simplenotes.app.jetty.Jetty
import be.simplenotes.app.routes.Router import be.simplenotes.app.routes.Router
import be.simplenotes.app.utils.StaticFileResolver import be.simplenotes.app.utils.StaticFileResolver
import be.simplenotes.app.utils.StaticFileResolverImpl import be.simplenotes.app.utils.StaticFileResolverImpl
import be.simplenotes.app.views.ErrorView import be.simplenotes.app.views.ErrorView
import be.simplenotes.config.ServerConfig import be.simplenotes.shared.config.ServerConfig
import org.eclipse.jetty.server.ServerConnector import org.eclipse.jetty.server.ServerConnector
import org.http4k.core.Filter
import org.http4k.core.RequestContexts import org.http4k.core.RequestContexts
import org.http4k.routing.RoutingHttpHandler import org.http4k.routing.RoutingHttpHandler
import org.http4k.server.ConnectorBuilder
import org.http4k.server.Jetty
import org.http4k.server.asServer import org.http4k.server.asServer
import org.koin.core.qualifier.named import org.koin.core.qualifier.named
import org.koin.core.qualifier.qualifier import org.koin.core.qualifier.qualifier
@@ -45,19 +43,16 @@ val serverModule = module {
get(), get(),
get(), get(),
get(), get(),
get(),
requiredAuth = get(AuthType.Required.qualifier), requiredAuth = get(AuthType.Required.qualifier),
optionalAuth = get(AuthType.Optional.qualifier), optionalAuth = get(AuthType.Optional.qualifier),
errorFilter = get(named("ErrorFilter")),
apiAuth = get(named("apiAuthFilter")), apiAuth = get(named("apiAuthFilter")),
get(), get()
get(),
get(),
)() )()
} }
single { RequestContexts() } single { RequestContexts() }
single<Filter>(AuthType.Optional.qualifier) { AuthFilter(get(), AuthType.Optional, get()) } single(AuthType.Optional.qualifier) { AuthFilter(get(), AuthType.Optional, get())() }
single<Filter>(AuthType.Required.qualifier) { AuthFilter(get(), AuthType.Required, get()) } single(AuthType.Required.qualifier) { AuthFilter(get(), AuthType.Required, get())() }
single { ErrorFilter(get()) } single(named("ErrorFilter")) { ErrorFilter(get())() }
single { TransactionFilter(get()) }
single { ErrorView(get()) } single { ErrorView(get()) }
} }
@@ -2,15 +2,19 @@ package be.simplenotes.app.routes
import be.simplenotes.app.api.ApiNoteController import be.simplenotes.app.api.ApiNoteController
import be.simplenotes.app.api.ApiUserController import be.simplenotes.app.api.ApiUserController
import be.simplenotes.app.controllers.* import be.simplenotes.app.controllers.BaseController
import be.simplenotes.app.filters.* import be.simplenotes.app.controllers.NoteController
import be.simplenotes.app.controllers.SettingsController
import be.simplenotes.app.controllers.UserController
import be.simplenotes.app.filters.ImmutableFilter
import be.simplenotes.app.filters.SecurityFilter
import be.simplenotes.app.filters.jwtPayload
import be.simplenotes.domain.security.JwtPayload import be.simplenotes.domain.security.JwtPayload
import org.http4k.core.* import org.http4k.core.*
import org.http4k.core.Method.* import org.http4k.core.Method.*
import org.http4k.filter.ResponseFilters.GZip import org.http4k.filter.ResponseFilters
import org.http4k.filter.ServerFilters.InitialiseRequestContext import org.http4k.filter.ServerFilters.InitialiseRequestContext
import org.http4k.routing.* import org.http4k.routing.*
import org.http4k.routing.ResourceLoader.Companion.Classpath
class Router( class Router(
private val baseController: BaseController, private val baseController: BaseController,
@@ -19,26 +23,26 @@ class Router(
private val settingsController: SettingsController, private val settingsController: SettingsController,
private val apiUserController: ApiUserController, private val apiUserController: ApiUserController,
private val apiNoteController: ApiNoteController, private val apiNoteController: ApiNoteController,
private val healthCheckController: HealthCheckController,
private val requiredAuth: Filter, private val requiredAuth: Filter,
private val optionalAuth: Filter, private val optionalAuth: Filter,
private val errorFilter: Filter,
private val apiAuth: Filter, private val apiAuth: Filter,
private val errorFilter: ErrorFilter,
private val transactionFilter: TransactionFilter,
private val contexts: RequestContexts, private val contexts: RequestContexts,
) { ) {
operator fun invoke(): RoutingHttpHandler { operator fun invoke(): RoutingHttpHandler {
val basicRoutes = val resourceLoader = ResourceLoader.Classpath(("/static"))
routes( val basicRoutes = routes(
"/health" bind GET to healthCheckController::healthCheck, ImmutableFilter().then(static(resourceLoader, "woff2" to ContentType("font/woff2"))),
ImmutableFilter.then(static(Classpath("/static"), "woff2" to ContentType("font/woff2")))
) )
val publicRoutes = routes( infix fun PathMethod.public(handler: PublicHandler) = this to { handler(it, it.jwtPayload(contexts)) }
infix fun PathMethod.protected(handler: ProtectedHandler) = this to { handler(it, it.jwtPayload(contexts)!!) }
val publicRoutes: RoutingHttpHandler = routes(
"/" bind GET public baseController::index, "/" bind GET public baseController::index,
"/register" bind GET public userController::register, "/register" bind GET public userController::register,
"/register" bind POST `public transactional` userController::register, "/register" bind POST public userController::register,
"/login" bind GET public userController::login, "/login" bind GET public userController::login,
"/login" bind POST public userController::login, "/login" bind POST public userController::login,
"/logout" bind POST to userController::logout, "/logout" bind POST to userController::logout,
@@ -47,18 +51,18 @@ class Router(
val protectedRoutes = routes( val protectedRoutes = routes(
"/settings" bind GET protected settingsController::settings, "/settings" bind GET protected settingsController::settings,
"/settings" bind POST transactional settingsController::settings, "/settings" bind POST protected settingsController::settings,
"/export" bind POST protected settingsController::export, "/export" bind POST protected settingsController::export,
"/notes" bind GET protected noteController::list, "/notes" bind GET protected noteController::list,
"/notes" bind POST protected noteController::search, "/notes" bind POST protected noteController::search,
"/notes/new" bind GET protected noteController::new, "/notes/new" bind GET protected noteController::new,
"/notes/new" bind POST transactional noteController::new, "/notes/new" bind POST protected noteController::new,
"/notes/trash" bind GET protected noteController::trash, "/notes/trash" bind GET protected noteController::trash,
"/notes/{uuid}" bind GET protected noteController::note, "/notes/{uuid}" bind GET protected noteController::note,
"/notes/{uuid}" bind POST transactional noteController::note, "/notes/{uuid}" bind POST protected noteController::note,
"/notes/{uuid}/edit" bind GET protected noteController::edit, "/notes/{uuid}/edit" bind GET protected noteController::edit,
"/notes/{uuid}/edit" bind POST transactional noteController::edit, "/notes/{uuid}/edit" bind POST protected noteController::edit,
"/notes/deleted/{uuid}" bind POST transactional noteController::deleted, "/notes/deleted/{uuid}" bind POST protected noteController::deleted,
) )
val apiRoutes = routes( val apiRoutes = routes(
@@ -67,10 +71,10 @@ class Router(
val protectedApiRoutes = routes( val protectedApiRoutes = routes(
"/api/notes" bind GET protected apiNoteController::notes, "/api/notes" bind GET protected apiNoteController::notes,
"/api/notes" bind POST transactional apiNoteController::createNote, "/api/notes" bind POST protected apiNoteController::createNote,
"/api/notes/search" bind POST transactional apiNoteController::search, "/api/notes/search" bind POST protected apiNoteController::search,
"/api/notes/{uuid}" bind GET protected apiNoteController::note, "/api/notes/{uuid}" bind GET protected apiNoteController::note,
"/api/notes/{uuid}" bind PUT transactional apiNoteController::update, "/api/notes/{uuid}" bind PUT protected apiNoteController::update,
) )
val routes = routes( val routes = routes(
@@ -83,23 +87,11 @@ class Router(
val globalFilters = errorFilter val globalFilters = errorFilter
.then(InitialiseRequestContext(contexts)) .then(InitialiseRequestContext(contexts))
.then(SecurityFilter) .then(SecurityFilter())
.then(GZip()) .then(ResponseFilters.GZip())
return globalFilters.then(routes) return globalFilters.then(routes)
} }
private inline infix fun PathMethod.public(crossinline handler: PublicHandler) =
this to { handler(it, it.jwtPayload(contexts)) }
private inline infix fun PathMethod.protected(crossinline handler: ProtectedHandler) =
this to { handler(it, it.jwtPayload(contexts)!!) }
private inline infix fun PathMethod.transactional(crossinline handler: ProtectedHandler) =
this to transactionFilter.then { handler(it, it.jwtPayload(contexts)!!) }
private inline infix fun PathMethod.`public transactional`(crossinline handler: PublicHandler) =
this to transactionFilter.then { handler(it, it.jwtPayload(contexts)) }
} }
private typealias PublicHandler = (Request, JwtPayload?) -> Response private typealias PublicHandler = (Request, JwtPayload?) -> Response
@@ -1,6 +1,6 @@
package be.simplenotes.app.utils package be.simplenotes.app.utils
import be.simplenotes.search.SearchTerms import be.simplenotes.domain.usecases.search.SearchTerms
private fun innerRegex(name: String) = private fun innerRegex(name: String) =
"""$name:['"](.*?)['"]""".toRegex() """$name:['"](.*?)['"]""".toRegex()
+115
View File
@@ -0,0 +1,115 @@
package be.simplenotes.app.views
import be.simplenotes.app.utils.StaticFileResolver
import be.simplenotes.domain.security.JwtPayload
import kotlinx.html.*
import kotlinx.html.div
import org.intellij.lang.annotations.Language
class BaseView(staticFileResolver: StaticFileResolver) : View(staticFileResolver) {
fun renderHome(jwtPayload: JwtPayload?) = renderPage(
title = "Home",
description = "A fast and simple note taking website",
jwtPayload = jwtPayload
) {
section("text-center my-2 p-2") {
h1("text-5xl casual") {
span("text-teal-300") { +"Simplenotes " }
+"- access your notes anywhere"
}
}
div("container mx-auto flex flex-wrap justify-center content-center") {
unsafe {
@Language("html")
val html =
"""
<div aria-label="demo" class="md:order-1 order-2 flipped p-4 my-10 w-full md:w-1/2">
<div class="flex justify-between mb-4">
<h1 class="text-2xl underline">Notes</h1>
<span>
<span class="btn btn-teal pointer-events-none">Trash (3)</span>
<span class="ml-2 btn btn-green pointer-events-none">New</span>
</span>
</div>
<form class="md:space-x-2" id="search">
<input aria-label="demo-search" name="search" disabled="" value="tag:&quot;demo&quot;">
<span id="buttons">
<button type="button" disabled="" class="btn btn-green pointer-events-none">search</button>
<span class="btn btn-red pointer-events-none">clear</span>
</span>
</form>
<div class="overflow-x-auto">
<table id="notes">
<thead>
<tr>
<th scope="col" class="w-1/2">Title</th>
<th scope="col" class="w-1/4">Updated</th>
<th scope="col" class="w-1/4">Tags</th>
</tr>
</thead>
<tbody>
<tr>
<td><span class="text-blue-200 font-semibold underline">Formula 1</span></td>
<td class="text-center">moments ago</td>
<td>
<ul class="inline flex flex-wrap justify-center">
<li class="mx-2 my-1"><span class="tag disabled">#demo</span ></li>
</ul>
</td>
</tr>
<tr>
<td><span class="text-blue-200 font-semibold underline">Syntax highlighting</span></td>
<td class="text-center">2 hours ago</td>
<td>
<ul class="inline flex flex-wrap justify-center">
<li class="mx-2 my-1"><span class="tag disabled">#features</span></li>
<li class="mx-2 my-1"><span class="tag disabled">#demo</span></li>
</ul>
</td>
</tr>
<tr>
<td><span class="text-blue-200 font-semibold underline">report</span></td>
<td class="text-center">5 days ago</td>
<td>
<ul class="inline flex flex-wrap justify-center">
<li class="mx-2 my-1"><span class="tag disabled">#study</span></li>
<li class="mx-2 my-1"><span class="tag disabled">#demo</span></li>
</ul>
</td>
</tr>
</tbody>
</table>
</div>
</div>
""".trimIndent()
+html
}
welcome()
}
}
@Suppress("NOTHING_TO_INLINE")
private inline fun DIV.welcome() {
div("w-full my-auto md:w-1/2 md:order-2 order-1 text-center") {
div("m-4 rounded-lg p-6") {
p("text-teal-400") {
h2("text-3xl text-teal-400 underline") { +"Features:" }
ul("list-disc text-lg list-inside") {
li { +"Markdown support" }
li { +"Full text search" }
li { +"Structured search" }
li { +"Code highlighting" }
li { +"Fast and lightweight" }
li { +"No tracking" }
li { +"Works without javascript" }
li { +"Data export" }
}
}
}
}
}
}
@@ -2,8 +2,8 @@ package be.simplenotes.app.views
import be.simplenotes.app.utils.StaticFileResolver import be.simplenotes.app.utils.StaticFileResolver
import be.simplenotes.app.views.components.* import be.simplenotes.app.views.components.*
import be.simplenotes.types.PersistedNote import be.simplenotes.domain.model.PersistedNote
import be.simplenotes.types.PersistedNoteMetadata import be.simplenotes.domain.model.PersistedNoteMetadata
import be.simplenotes.domain.security.JwtPayload import be.simplenotes.domain.security.JwtPayload
import io.konform.validation.ValidationError import io.konform.validation.ValidationError
import kotlinx.html.* import kotlinx.html.*
@@ -143,6 +143,7 @@ class NoteView(staticFileResolver: StaticFileResolver) : View(staticFileResolver
} }
if (!shared) { if (!shared) {
noteActionForm(note) noteActionForm(note)
publicPrivateForm(note)
if (note.public) { if (note.public) {
p("my-4") { p("my-4") {
@@ -165,29 +166,12 @@ class NoteView(staticFileResolver: StaticFileResolver) : View(staticFileResolver
} }
private fun DIV.noteActionForm(note: PersistedNote) { private fun DIV.noteActionForm(note: PersistedNote) {
form(method = FormMethod.post, classes = "inline flex space-x-2 justify-end mb-4") { span("flex space-x-2 justify-end mb-4") {
a( a(
href = "/notes/${note.uuid}/edit", href = "/notes/${note.uuid}/edit",
classes = "btn btn-green" classes = "btn btn-teal"
) { +"Edit" } ) { +"Edit" }
span { form(method = FormMethod.post, classes = "inline") {
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"
) {
+"Private"
}
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-r bg-teal-200 text-gray-800"
) {
+"Public"
}
}
button( button(
type = ButtonType.submit, type = ButtonType.submit,
name = "delete", name = "delete",
@@ -195,4 +179,23 @@ class NoteView(staticFileResolver: StaticFileResolver) : View(staticFileResolver
) { +"Delete" } ) { +"Delete" }
} }
} }
}
private fun DIV.publicPrivateForm(note: PersistedNote) {
span("flex space-x-2 justify-end mb-4") {
form(method = FormMethod.post, classes = "ml-auto ") {
button(
type = ButtonType.submit,
name = if (note.public) "private" else "public",
classes = "btn btn-teal"
) {
if (note.public)
+"This note is public, do you want to make it private ?"
else
+"This note is private, do you want to make it public ?"
}
}
}
}
} }
@@ -26,23 +26,22 @@ class SettingView(staticFileResolver: StaticFileResolver) : View(staticFileResol
} }
} }
section("m-4 p-2 bg-gray-800 rounded flex flex-wrap justify-around items-end") { section("m-4 p-4 bg-gray-800 rounded flex justify-around") {
form(classes = "m-2", method = FormMethod.post, action = "/export") { form(method = FormMethod.post, action = "/export") {
button(name = "display", button(name = "display",
classes = "inline btn btn-teal block", classes = "inline btn btn-teal block",
type = submit) { +"Display my data" } type = submit) { +"Display my data" }
} }
form(classes = "m-2", method = FormMethod.post, action = "/export") { form(method = FormMethod.post, action = "/export") {
div {
listOf("json", "zip").forEach { format -> listOf("json", "zip").forEach { format ->
div {
radioInput(name = "format") { radioInput(name = "format") {
id = format id = format
attributes["value"] = format attributes["value"] = format
if (format == "json") attributes["checked"] = "" if(format == "json") attributes["checked"] = ""
else attributes["class"] = "ml-4"
} }
label(classes = "ml-2") { label(classes = "ml-2") {
attributes["for"] = format attributes["for"] = format
@@ -31,7 +31,7 @@ abstract class View(staticFileResolver: StaticFileResolver) {
attributes["crossorigin"] = "anonymous" attributes["crossorigin"] = "anonymous"
} }
link(rel = "stylesheet", href = styles) link(rel = "stylesheet", href = styles)
icons() link(rel = "shortcut icon", href = "/favicon.ico", type = "image/x-icon")
scripts.forEach { src -> scripts.forEach { src ->
script(src = src) {} script(src = src) {}
} }
@@ -42,15 +42,4 @@ abstract class View(staticFileResolver: StaticFileResolver) {
} }
} }
} }
@Suppress("NOTHING_TO_INLINE")
private inline fun HEAD.icons() {
link(rel = "apple-touch-icon", href = "/apple-touch-icon.png") { attributes["sizes"] = "180x180" }
link(rel = "icon", href = "/favicon-32x32.png", type = "image/png") { attributes["sizes"] = "32x32" }
link(rel = "icon", href = "/favicon-16x16.png", type = "image/png") { attributes["sizes"] = "16x16" }
link(rel = "manifest", href = "/site.webmanifest")
link(rel = "mask-icon", href = "/safari-pinned-tab.svg") { attributes["color"] = "#2c7a7b" }
meta(name = "msapplication-TileColor", content = "#00aba9")
meta(name = "theme-color", content = "#2c7a7b")
}
} }
@@ -1,7 +1,7 @@
package be.simplenotes.app.views.components package be.simplenotes.app.views.components
import be.simplenotes.app.utils.toTimeAgo import be.simplenotes.app.utils.toTimeAgo
import be.simplenotes.types.PersistedNoteMetadata import be.simplenotes.domain.model.PersistedNoteMetadata
import kotlinx.html.* import kotlinx.html.*
import kotlinx.html.ButtonType.submit import kotlinx.html.ButtonType.submit
import kotlinx.html.FormMethod.post import kotlinx.html.FormMethod.post
@@ -25,8 +25,8 @@ fun FlowContent.deletedNoteTable(notes: List<PersistedNoteMetadata>) = div("over
td("text-center") { +updatedAt.toTimeAgo() } td("text-center") { +updatedAt.toTimeAgo() }
td { tags(tags) } td { tags(tags) }
td("text-center") { td("text-center") {
form(method = post, action = "/notes/deleted/$uuid") { form(classes = "inline", method = post, action = "/notes/deleted/$uuid") {
button(classes = "btn btn-red mb-2", type = submit, name = "delete") { button(classes = "btn btn-red", type = submit, name = "delete") {
+"Delete permanently" +"Delete permanently"
} }
button(classes = "ml-2 btn btn-green", type = submit, name = "restore") { button(classes = "ml-2 btn btn-green", type = submit, name = "restore") {
@@ -1,7 +1,7 @@
package be.simplenotes.app.views.components package be.simplenotes.app.views.components
import be.simplenotes.app.utils.toTimeAgo import be.simplenotes.app.utils.toTimeAgo
import be.simplenotes.types.PersistedNoteMetadata import be.simplenotes.domain.model.PersistedNoteMetadata
import kotlinx.html.* import kotlinx.html.*
import kotlinx.html.ThScope.col import kotlinx.html.ThScope.col
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

+1
View File
@@ -0,0 +1 @@
package be.simplenotes.app
@@ -3,7 +3,7 @@ package be.simplenotes.app.filters
import be.simplenotes.domain.security.JwtPayload import be.simplenotes.domain.security.JwtPayload
import be.simplenotes.domain.security.JwtPayloadExtractor import be.simplenotes.domain.security.JwtPayloadExtractor
import be.simplenotes.domain.security.SimpleJwt import be.simplenotes.domain.security.SimpleJwt
import be.simplenotes.config.JwtConfig import be.simplenotes.shared.config.JwtConfig
import com.natpryce.hamkrest.assertion.assertThat import com.natpryce.hamkrest.assertion.assertThat
import org.http4k.core.* import org.http4k.core.*
import org.http4k.core.Method.GET import org.http4k.core.Method.GET
@@ -27,8 +27,8 @@ internal class AuthFilterTest {
private val simpleJwt = SimpleJwt(jwtConfig) private val simpleJwt = SimpleJwt(jwtConfig)
private val extractor = JwtPayloadExtractor(simpleJwt) private val extractor = JwtPayloadExtractor(simpleJwt)
private val ctx = RequestContexts() private val ctx = RequestContexts()
private val requiredAuth = AuthFilter(extractor, AuthType.Required, ctx) private val requiredAuth = AuthFilter(extractor, AuthType.Required, ctx)()
private val optionalAuth = AuthFilter(extractor, AuthType.Optional, ctx) private val optionalAuth = AuthFilter(extractor, AuthType.Optional, ctx)()
private val echoJwtPayloadHandler = { request: Request -> Response(OK).body(request.jwtPayload(ctx).toString()) } private val echoJwtPayloadHandler = { request: Request -> Response(OK).body(request.jwtPayload(ctx).toString()) }
@@ -1,6 +1,6 @@
package be.simplenotes.app.utils package be.simplenotes.app.utils
import be.simplenotes.search.SearchTerms import be.simplenotes.domain.usecases.search.SearchTerms
import org.assertj.core.api.Assertions.assertThat import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.params.ParameterizedTest import org.junit.jupiter.params.ParameterizedTest
import org.junit.jupiter.params.provider.MethodSource import org.junit.jupiter.params.provider.MethodSource
+1 -5
View File
@@ -11,11 +11,7 @@ simplenotes.be {
import strict-transport import strict-transport
header -Server header -Server
reverse_proxy http://localhost:8080 { reverse_proxy http://localhost:8080
health_path /health
health_interval 5s
health_timeout 200ms
}
} }
dev.simplenotes.be { dev.simplenotes.be {
+2 -2
View File
@@ -2,8 +2,8 @@
"name": "css", "name": "css",
"version": "1.0.0", "version": "1.0.0",
"scripts": { "scripts": {
"css": "NODE_ENV=dev MANIFEST=../simplenotes-app/src/main/resources/css-manifest.json postcss build src/styles.pcss --output ../simplenotes-app/src/main/resources/static/styles.css", "css": "NODE_ENV=dev MANIFEST=../app/src/main/resources/css-manifest.json postcss build src/styles.pcss --output ../app/src/main/resources/static/styles.css",
"css-purge": "NODE_ENV=production MANIFEST=../simplenotes-app/src/main/resources/css-manifest.json postcss build src/styles.pcss --output ../simplenotes-app/src/main/resources/static/styles.css" "css-purge": "NODE_ENV=production MANIFEST=../app/src/main/resources/css-manifest.json postcss build src/styles.pcss --output ../app/src/main/resources/static/styles.css"
}, },
"dependencies": { "dependencies": {
"autoprefixer": "^9.8.6", "autoprefixer": "^9.8.6",
+1 -1
View File
@@ -1,7 +1,7 @@
module.exports = { module.exports = {
purge: { purge: {
content: [ content: [
'../simplenotes-app/src/main/kotlin/be/simplenotes/app/views/**/*.kt' '../app/src/main/kotlin/views/**/*.kt'
] ]
}, },
theme: { theme: {
+2 -2
View File
@@ -1,7 +1,7 @@
#!/bin/sh #!/bin/sh
rm simplenotes-app/src/main/resources/css-manifest.json rm app/src/main/resources/css-manifest.json
rm simplenotes-app/src/main/resources/static/styles* rm app/src/main/resources/static/styles*
yarn --cwd css run css-purge \ yarn --cwd css run css-purge \
&& docker build -t hubv/simplenotes:latest . \ && docker build -t hubv/simplenotes:latest . \
+2 -10
View File
@@ -19,10 +19,8 @@ services:
volumes: volumes:
- notes-db-volume:/var/lib/mysql - notes-db-volume:/var/lib/mysql
healthcheck: healthcheck:
test: "mysql --protocol=tcp -u simplenotes -p$MYSQL_PASSWORD -e 'show databases'" test: ["CMD", "mysqladmin" ,"ping", "-h", "localhost"]
interval: 5s timeout: 10s
timeout: 1s
start_period: 2s
retries: 10 retries: 10
simplenotes: simplenotes:
@@ -41,12 +39,6 @@ services:
# - PASSWORD # - PASSWORD
ports: ports:
- 127.0.0.1:8080:8080 - 127.0.0.1:8080:8080
healthcheck:
test: "curl --fail -s http://localhost:8080/health"
interval: 5s
timeout: 1s
start_period: 2s
retries: 3
depends_on: depends_on:
db: db:
condition: service_healthy condition: service_healthy
+9 -51
View File
@@ -1,55 +1,27 @@
<?xml version="1.0" encoding="UTF-8"?> <project>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent> <parent>
<artifactId>simplenotes-parent</artifactId> <artifactId>parent</artifactId>
<groupId>be.simplenotes</groupId> <groupId>be.simplenotes</groupId>
<version>1.0-SNAPSHOT</version> <version>1.0-SNAPSHOT</version>
</parent> </parent>
<modelVersion>4.0.0</modelVersion> <modelVersion>4.0.0</modelVersion>
<artifactId>simplenotes-domain</artifactId> <artifactId>domain</artifactId>
<dependencies> <dependencies>
<dependency> <dependency>
<groupId>be.simplenotes</groupId> <groupId>be.simplenotes</groupId>
<artifactId>simplenotes-config</artifactId> <artifactId>shared</artifactId>
<version>1.0-SNAPSHOT</version> <version>1.0-SNAPSHOT</version>
</dependency> </dependency>
<dependency> <dependency>
<groupId>be.simplenotes</groupId> <groupId>be.simplenotes</groupId>
<artifactId>simplenotes-test-resources</artifactId> <artifactId>shared</artifactId>
<version>1.0-SNAPSHOT</version> <version>1.0-SNAPSHOT</version>
<type>test-jar</type> <type>test-jar</type>
<scope>test</scope> <scope>test</scope>
</dependency> </dependency>
<dependency>
<groupId>org.koin</groupId>
<artifactId>koin-core</artifactId>
</dependency>
<dependency>
<groupId>io.arrow-kt</groupId>
<artifactId>arrow-core</artifactId>
</dependency>
<dependency>
<groupId>com.natpryce</groupId>
<artifactId>hamkrest</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.mockk</groupId>
<artifactId>mockk</artifactId>
<scope>test</scope>
</dependency>
<dependency> <dependency>
<groupId>io.konform</groupId> <groupId>io.konform</groupId>
<artifactId>konform-jvm</artifactId> <artifactId>konform-jvm</artifactId>
@@ -85,29 +57,15 @@
<artifactId>owasp-java-html-sanitizer</artifactId> <artifactId>owasp-java-html-sanitizer</artifactId>
<version>20200713.1</version> <version>20200713.1</version>
</dependency> </dependency>
<dependency>
<groupId>org.jetbrains.kotlinx</groupId>
<artifactId>kotlinx-serialization-runtime</artifactId>
</dependency>
<dependency> <dependency>
<groupId>org.apache.commons</groupId> <groupId>org.apache.commons</groupId>
<artifactId>commons-compress</artifactId> <artifactId>commons-compress</artifactId>
<version>1.20</version> <version>1.20</version>
</dependency> </dependency>
<dependency>
<groupId>be.simplenotes</groupId>
<artifactId>simplenotes-types</artifactId>
<version>1.0-SNAPSHOT</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>be.simplenotes</groupId>
<artifactId>simplenotes-persistance</artifactId>
<version>1.0-SNAPSHOT</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>be.simplenotes</groupId>
<artifactId>simplenotes-search</artifactId>
<version>1.0-SNAPSHOT</version>
<scope>compile</scope>
</dependency>
</dependencies> </dependencies>
</project> </project>
@@ -1,4 +1,4 @@
package be.simplenotes.types package be.simplenotes.domain.model
import kotlinx.serialization.Contextual import kotlinx.serialization.Contextual
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
@@ -1,4 +1,4 @@
package be.simplenotes.types package be.simplenotes.domain.model
data class User(val username: String, val password: String) data class User(val username: String, val password: String)
data class PersistedUser(val username: String, val password: String, val id: Int) data class PersistedUser(val username: String, val password: String, val id: Int)
@@ -2,7 +2,7 @@ package be.simplenotes.domain.security
import org.owasp.html.HtmlPolicyBuilder import org.owasp.html.HtmlPolicyBuilder
internal object HtmlSanitizer { object HtmlSanitizer {
private val htmlPolicy = HtmlPolicyBuilder() private val htmlPolicy = HtmlPolicyBuilder()
.allowElements("a") .allowElements("a")
.allowCommonBlockElements() .allowCommonBlockElements()
@@ -1,6 +1,6 @@
package be.simplenotes.domain.security package be.simplenotes.domain.security
import be.simplenotes.types.PersistedUser import be.simplenotes.domain.model.PersistedUser
import com.auth0.jwt.exceptions.JWTVerificationException import com.auth0.jwt.exceptions.JWTVerificationException
data class JwtPayload(val userId: Int, val username: String) { data class JwtPayload(val userId: Int, val username: String) {
@@ -1,6 +1,6 @@
package be.simplenotes.domain.security package be.simplenotes.domain.security
import be.simplenotes.config.JwtConfig import be.simplenotes.shared.config.JwtConfig
import com.auth0.jwt.JWT import com.auth0.jwt.JWT
import com.auth0.jwt.JWTVerifier import com.auth0.jwt.JWTVerifier
import com.auth0.jwt.algorithms.Algorithm import com.auth0.jwt.algorithms.Algorithm
@@ -2,16 +2,16 @@ package be.simplenotes.domain.usecases
import arrow.core.Either import arrow.core.Either
import arrow.core.extensions.fx import arrow.core.extensions.fx
import be.simplenotes.types.Note import be.simplenotes.domain.model.Note
import be.simplenotes.types.PersistedNote import be.simplenotes.domain.model.PersistedNote
import be.simplenotes.types.PersistedNoteMetadata import be.simplenotes.domain.model.PersistedNoteMetadata
import be.simplenotes.domain.security.HtmlSanitizer import be.simplenotes.domain.security.HtmlSanitizer
import be.simplenotes.domain.usecases.markdown.MarkdownConverter import be.simplenotes.domain.usecases.markdown.MarkdownConverter
import be.simplenotes.domain.usecases.markdown.MarkdownParsingError import be.simplenotes.domain.usecases.markdown.MarkdownParsingError
import be.simplenotes.persistance.repositories.NoteRepository import be.simplenotes.domain.usecases.repositories.NoteRepository
import be.simplenotes.persistance.repositories.UserRepository import be.simplenotes.domain.usecases.repositories.UserRepository
import be.simplenotes.search.NoteSearcher import be.simplenotes.domain.usecases.search.NoteSearcher
import be.simplenotes.search.SearchTerms import be.simplenotes.domain.usecases.search.SearchTerms
import java.util.* import java.util.*
class NoteService( class NoteService(
@@ -1,7 +1,7 @@
package be.simplenotes.domain.usecases.export package be.simplenotes.domain.usecases.export
import be.simplenotes.types.ExportedNote import be.simplenotes.domain.model.ExportedNote
import be.simplenotes.persistance.repositories.NoteRepository import be.simplenotes.domain.usecases.repositories.NoteRepository
import kotlinx.serialization.builtins.ListSerializer import kotlinx.serialization.builtins.ListSerializer
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import org.apache.commons.compress.archivers.zip.ZipArchiveEntry import org.apache.commons.compress.archivers.zip.ZipArchiveEntry
@@ -4,7 +4,7 @@ import arrow.core.Either
import arrow.core.extensions.fx import arrow.core.extensions.fx
import arrow.core.left import arrow.core.left
import arrow.core.right import arrow.core.right
import be.simplenotes.types.NoteMetadata import be.simplenotes.domain.model.NoteMetadata
import be.simplenotes.domain.validation.NoteValidations import be.simplenotes.domain.validation.NoteValidations
import com.vladsch.flexmark.ext.gfm.tasklist.TaskListExtension import com.vladsch.flexmark.ext.gfm.tasklist.TaskListExtension
import com.vladsch.flexmark.html.HtmlRenderer import com.vladsch.flexmark.html.HtmlRenderer
@@ -1,9 +1,9 @@
package be.simplenotes.persistance.repositories package be.simplenotes.domain.usecases.repositories
import be.simplenotes.types.ExportedNote import be.simplenotes.domain.model.ExportedNote
import be.simplenotes.types.Note import be.simplenotes.domain.model.Note
import be.simplenotes.types.PersistedNote import be.simplenotes.domain.model.PersistedNote
import be.simplenotes.types.PersistedNoteMetadata import be.simplenotes.domain.model.PersistedNoteMetadata
import java.util.* import java.util.*
interface NoteRepository { interface NoteRepository {
@@ -1,7 +1,7 @@
package be.simplenotes.persistance.repositories package be.simplenotes.domain.usecases.repositories
import be.simplenotes.types.PersistedUser import be.simplenotes.domain.model.PersistedUser
import be.simplenotes.types.User import be.simplenotes.domain.model.User
interface UserRepository { interface UserRepository {
fun create(user: User): PersistedUser? fun create(user: User): PersistedUser?
@@ -1,7 +1,7 @@
package be.simplenotes.search package be.simplenotes.domain.usecases.search
import be.simplenotes.types.PersistedNote import be.simplenotes.domain.model.PersistedNote
import be.simplenotes.types.PersistedNoteMetadata import be.simplenotes.domain.model.PersistedNoteMetadata
import java.util.* import java.util.*
data class SearchTerms(val title: String?, val tag: String?, val content: String?, val all: String?) data class SearchTerms(val title: String?, val tag: String?, val content: String?, val all: String?)
@@ -4,9 +4,9 @@ import arrow.core.Either
import arrow.core.extensions.fx import arrow.core.extensions.fx
import arrow.core.rightIfNotNull import arrow.core.rightIfNotNull
import be.simplenotes.domain.security.PasswordHash import be.simplenotes.domain.security.PasswordHash
import be.simplenotes.persistance.repositories.UserRepository import be.simplenotes.domain.usecases.repositories.UserRepository
import be.simplenotes.domain.usecases.search.NoteSearcher
import be.simplenotes.domain.validation.UserValidations import be.simplenotes.domain.validation.UserValidations
import be.simplenotes.search.NoteSearcher
internal class DeleteUseCaseImpl( internal class DeleteUseCaseImpl(
private val userRepository: UserRepository, private val userRepository: UserRepository,
@@ -7,8 +7,8 @@ import arrow.core.rightIfNotNull
import be.simplenotes.domain.security.JwtPayload import be.simplenotes.domain.security.JwtPayload
import be.simplenotes.domain.security.PasswordHash import be.simplenotes.domain.security.PasswordHash
import be.simplenotes.domain.security.SimpleJwt import be.simplenotes.domain.security.SimpleJwt
import be.simplenotes.domain.usecases.repositories.UserRepository
import be.simplenotes.domain.validation.UserValidations import be.simplenotes.domain.validation.UserValidations
import be.simplenotes.persistance.repositories.UserRepository
internal class LoginUseCaseImpl( internal class LoginUseCaseImpl(
private val userRepository: UserRepository, private val userRepository: UserRepository,
@@ -3,10 +3,10 @@ package be.simplenotes.domain.usecases.users.register
import arrow.core.Either import arrow.core.Either
import arrow.core.filterOrElse import arrow.core.filterOrElse
import arrow.core.leftIfNull import arrow.core.leftIfNull
import be.simplenotes.types.PersistedUser import be.simplenotes.domain.model.PersistedUser
import be.simplenotes.domain.security.PasswordHash import be.simplenotes.domain.security.PasswordHash
import be.simplenotes.domain.usecases.repositories.UserRepository
import be.simplenotes.domain.validation.UserValidations import be.simplenotes.domain.validation.UserValidations
import be.simplenotes.persistance.repositories.UserRepository
internal class RegisterUseCaseImpl( internal class RegisterUseCaseImpl(
private val userRepository: UserRepository, private val userRepository: UserRepository,
@@ -1,7 +1,7 @@
package be.simplenotes.domain.usecases.users.register package be.simplenotes.domain.usecases.users.register
import arrow.core.Either import arrow.core.Either
import be.simplenotes.types.PersistedUser import be.simplenotes.domain.model.PersistedUser
import be.simplenotes.domain.usecases.users.login.LoginForm import be.simplenotes.domain.usecases.users.login.LoginForm
import io.konform.validation.ValidationErrors import io.konform.validation.ValidationErrors
@@ -1,7 +1,7 @@
package be.simplenotes.domain.validation package be.simplenotes.domain.validation
import arrow.core.* import arrow.core.*
import be.simplenotes.types.NoteMetadata import be.simplenotes.domain.model.NoteMetadata
import be.simplenotes.domain.usecases.markdown.ValidationError import be.simplenotes.domain.usecases.markdown.ValidationError
import io.konform.validation.Validation import io.konform.validation.Validation
import io.konform.validation.jsonschema.maxItems import io.konform.validation.jsonschema.maxItems
@@ -3,7 +3,7 @@ package be.simplenotes.domain.validation
import arrow.core.Either import arrow.core.Either
import arrow.core.left import arrow.core.left
import arrow.core.right import arrow.core.right
import be.simplenotes.types.User import be.simplenotes.domain.model.User
import be.simplenotes.domain.usecases.users.delete.DeleteError import be.simplenotes.domain.usecases.users.delete.DeleteError
import be.simplenotes.domain.usecases.users.delete.DeleteForm import be.simplenotes.domain.usecases.users.delete.DeleteForm
import be.simplenotes.domain.usecases.users.login.InvalidLoginForm import be.simplenotes.domain.usecases.users.login.InvalidLoginForm
+5
View File
@@ -0,0 +1,5 @@
package be.simplenotes.domain
/**
* Empty file @see [root-package-declaration](https://discuss.kotlinlang.org/t/root-package-declaration-to-reduce-folder-clutter/2247/4)
*/
@@ -1,7 +1,7 @@
package be.simplenotes.domain.security package be.simplenotes.domain.security
import be.simplenotes.domain.usecases.users.login.Token import be.simplenotes.domain.usecases.users.login.Token
import be.simplenotes.config.JwtConfig import be.simplenotes.shared.config.JwtConfig
import com.auth0.jwt.JWT import com.auth0.jwt.JWT
import com.auth0.jwt.algorithms.Algorithm import com.auth0.jwt.algorithms.Algorithm
import com.natpryce.hamkrest.absent import com.natpryce.hamkrest.absent
@@ -1,12 +1,12 @@
package be.simplenotes.domain.usecases.users.login package be.simplenotes.domain.usecases.users.login
import be.simplenotes.types.PersistedUser import be.simplenotes.domain.model.PersistedUser
import be.simplenotes.domain.security.BcryptPasswordHash import be.simplenotes.domain.security.BcryptPasswordHash
import be.simplenotes.domain.security.SimpleJwt import be.simplenotes.domain.security.SimpleJwt
import be.simplenotes.persistance.repositories.UserRepository import be.simplenotes.domain.usecases.repositories.UserRepository
import be.simplenotes.config.JwtConfig import be.simplenotes.shared.config.JwtConfig
import be.simplenotes.domain.testutils.isLeftOfType import be.simplenotes.shared.testutils.assertions.isLeftOfType
import be.simplenotes.domain.testutils.isRight import be.simplenotes.shared.testutils.assertions.isRight
import com.natpryce.hamkrest.assertion.assertThat import com.natpryce.hamkrest.assertion.assertThat
import io.mockk.* import io.mockk.*
import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.BeforeEach
@@ -1,10 +1,10 @@
package be.simplenotes.domain.usecases.users.register package be.simplenotes.domain.usecases.users.register
import be.simplenotes.types.PersistedUser import be.simplenotes.domain.model.PersistedUser
import be.simplenotes.domain.security.BcryptPasswordHash import be.simplenotes.domain.security.BcryptPasswordHash
import be.simplenotes.domain.testutils.isLeftOfType import be.simplenotes.domain.usecases.repositories.UserRepository
import be.simplenotes.domain.testutils.isRight import be.simplenotes.shared.testutils.assertions.isLeftOfType
import be.simplenotes.persistance.repositories.UserRepository import be.simplenotes.shared.testutils.assertions.isRight
import com.natpryce.hamkrest.assertion.assertThat import com.natpryce.hamkrest.assertion.assertThat
import com.natpryce.hamkrest.equalTo import com.natpryce.hamkrest.equalTo
import io.mockk.* import io.mockk.*
@@ -1,10 +1,10 @@
package be.simplenotes.domain.validation package be.simplenotes.domain.validation
import be.simplenotes.domain.testutils.isLeftOfType
import be.simplenotes.domain.testutils.isRight
import be.simplenotes.domain.usecases.users.login.InvalidLoginForm import be.simplenotes.domain.usecases.users.login.InvalidLoginForm
import be.simplenotes.domain.usecases.users.login.LoginForm import be.simplenotes.domain.usecases.users.login.LoginForm
import be.simplenotes.domain.usecases.users.register.RegisterForm import be.simplenotes.domain.usecases.users.register.RegisterForm
import be.simplenotes.shared.testutils.assertions.isLeftOfType
import be.simplenotes.shared.testutils.assertions.isRight
import com.natpryce.hamkrest.assertion.assertThat import com.natpryce.hamkrest.assertion.assertThat
import org.junit.jupiter.api.Nested import org.junit.jupiter.api.Nested
import org.junit.jupiter.params.ParameterizedTest import org.junit.jupiter.params.ParameterizedTest
@@ -1,54 +1,32 @@
<?xml version="1.0" encoding="UTF-8"?> <project>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent> <parent>
<artifactId>simplenotes-parent</artifactId> <artifactId>parent</artifactId>
<groupId>be.simplenotes</groupId> <groupId>be.simplenotes</groupId>
<version>1.0-SNAPSHOT</version> <version>1.0-SNAPSHOT</version>
</parent> </parent>
<modelVersion>4.0.0</modelVersion> <modelVersion>4.0.0</modelVersion>
<artifactId>simplenotes-persistance</artifactId> <artifactId>persistance</artifactId>
<dependencies> <dependencies>
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct</artifactId>
</dependency>
<dependency> <dependency>
<groupId>be.simplenotes</groupId> <groupId>be.simplenotes</groupId>
<artifactId>simplenotes-types</artifactId> <artifactId>domain</artifactId>
<version>1.0-SNAPSHOT</version> <version>1.0-SNAPSHOT</version>
</dependency> </dependency>
<dependency> <dependency>
<groupId>be.simplenotes</groupId> <groupId>be.simplenotes</groupId>
<artifactId>simplenotes-config</artifactId> <artifactId>shared</artifactId>
<version>1.0-SNAPSHOT</version> <version>1.0-SNAPSHOT</version>
</dependency> </dependency>
<dependency> <dependency>
<groupId>be.simplenotes</groupId> <groupId>be.simplenotes</groupId>
<artifactId>simplenotes-test-resources</artifactId> <artifactId>shared</artifactId>
<version>1.0-SNAPSHOT</version> <version>1.0-SNAPSHOT</version>
<type>test-jar</type> <type>test-jar</type>
<scope>test</scope> <scope>test</scope>
</dependency> </dependency>
<dependency>
<groupId>org.koin</groupId>
<artifactId>koin-core</artifactId>
</dependency>
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
<dependency> <dependency>
<groupId>org.mariadb.jdbc</groupId> <groupId>org.mariadb.jdbc</groupId>
<artifactId>mariadb-java-client</artifactId> <artifactId>mariadb-java-client</artifactId>
@@ -64,20 +42,20 @@
<artifactId>flyway-core</artifactId> <artifactId>flyway-core</artifactId>
<version>6.5.4</version> <version>6.5.4</version>
</dependency> </dependency>
<dependency> <dependency>
<groupId>com.zaxxer</groupId> <groupId>com.zaxxer</groupId>
<artifactId>HikariCP</artifactId> <artifactId>HikariCP</artifactId>
<version>3.4.3</version> <version>3.4.5</version>
</dependency> </dependency>
<dependency> <dependency>
<groupId>me.liuwj.ktorm</groupId> <groupId>me.liuwj.ktorm</groupId>
<artifactId>ktorm-core</artifactId> <artifactId>ktorm-core</artifactId>
<version>3.0.0</version>
</dependency> </dependency>
<dependency> <dependency>
<groupId>me.liuwj.ktorm</groupId> <groupId>me.liuwj.ktorm</groupId>
<artifactId>ktorm-support-mysql</artifactId> <artifactId>ktorm-support-mysql</artifactId>
<version>3.0.0</version>
</dependency> </dependency>
</dependencies> </dependencies>
@@ -0,0 +1,24 @@
package be.simplenotes.persistance
import be.simplenotes.shared.config.DataSourceConfig
import org.flywaydb.core.Flyway
import javax.sql.DataSource
internal class DbMigrationsImpl(
private val dataSource: DataSource,
private val dataSourceConfig: DataSourceConfig
) : DbMigrations {
override fun migrate() {
val migrationDir = when {
dataSourceConfig.jdbcUrl.contains("mariadb") -> "db/migration/mariadb"
else -> "db/migration/other"
}
Flyway.configure()
.dataSource(dataSource)
.locations(migrationDir)
.load()
.migrate()
}
}
@@ -1,14 +1,10 @@
package be.simplenotes.persistance package be.simplenotes.persistance
import be.simplenotes.config.DataSourceConfig import be.simplenotes.domain.usecases.repositories.NoteRepository
import be.simplenotes.persistance.converters.NoteConverter import be.simplenotes.domain.usecases.repositories.UserRepository
import be.simplenotes.persistance.converters.NoteConverterImpl
import be.simplenotes.persistance.converters.UserConverter
import be.simplenotes.persistance.converters.UserConverterImpl
import be.simplenotes.persistance.notes.NoteRepositoryImpl import be.simplenotes.persistance.notes.NoteRepositoryImpl
import be.simplenotes.persistance.repositories.NoteRepository
import be.simplenotes.persistance.repositories.UserRepository
import be.simplenotes.persistance.users.UserRepositoryImpl import be.simplenotes.persistance.users.UserRepositoryImpl
import be.simplenotes.shared.config.DataSourceConfig
import com.zaxxer.hikari.HikariConfig import com.zaxxer.hikari.HikariConfig
import com.zaxxer.hikari.HikariDataSource import com.zaxxer.hikari.HikariDataSource
import me.liuwj.ktorm.database.Database import me.liuwj.ktorm.database.Database
@@ -17,6 +13,10 @@ import org.koin.dsl.module
import org.koin.dsl.onClose import org.koin.dsl.onClose
import javax.sql.DataSource import javax.sql.DataSource
interface DbMigrations {
fun migrate()
}
private fun hikariDataSource(conf: DataSourceConfig): HikariDataSource { private fun hikariDataSource(conf: DataSourceConfig): HikariDataSource {
val hikariConfig = HikariConfig().also { val hikariConfig = HikariConfig().also {
it.jdbcUrl = conf.jdbcUrl it.jdbcUrl = conf.jdbcUrl
@@ -34,14 +34,11 @@ val migrationModule = module {
} }
val persistanceModule = module { val persistanceModule = module {
single<NoteConverter> { NoteConverterImpl() } single<UserRepository> { UserRepositoryImpl(get()) }
single<UserConverter> { UserConverterImpl() } single<NoteRepository> { NoteRepositoryImpl(get()) }
single<UserRepository> { UserRepositoryImpl(get(), get()) }
single<NoteRepository> { NoteRepositoryImpl(get(), get()) }
single { hikariDataSource(get()) } bind DataSource::class onClose { it?.close() } single { hikariDataSource(get()) } bind DataSource::class onClose { it?.close() }
single { single {
get<DbMigrations>().migrate() get<DbMigrations>().migrate()
Database.connect(get<DataSource>()) Database.connect(get<DataSource>())
} }
single<DbHealthCheck> { DbHealthCheckImpl(get(), get()) }
} }
@@ -1,11 +1,7 @@
package be.simplenotes.persistance.notes package be.simplenotes.persistance.notes
import be.simplenotes.types.ExportedNote import be.simplenotes.domain.model.*
import be.simplenotes.types.Note import be.simplenotes.domain.usecases.repositories.NoteRepository
import be.simplenotes.types.PersistedNote
import be.simplenotes.types.PersistedNoteMetadata
import be.simplenotes.persistance.converters.NoteConverter
import be.simplenotes.persistance.repositories.NoteRepository
import me.liuwj.ktorm.database.Database import me.liuwj.ktorm.database.Database
import me.liuwj.ktorm.dsl.* import me.liuwj.ktorm.dsl.*
import me.liuwj.ktorm.entity.* import me.liuwj.ktorm.entity.*
@@ -13,7 +9,7 @@ import java.time.LocalDateTime
import java.util.* import java.util.*
import kotlin.collections.HashMap import kotlin.collections.HashMap
internal class NoteRepositoryImpl(private val db: Database, private val converter: NoteConverter) : NoteRepository { internal class NoteRepositoryImpl(private val db: Database) : NoteRepository {
@Throws(IllegalArgumentException::class) @Throws(IllegalArgumentException::class)
override fun findAll( override fun findAll(
@@ -21,7 +17,7 @@ internal class NoteRepositoryImpl(private val db: Database, private val converte
limit: Int, limit: Int,
offset: Int, offset: Int,
tag: String?, tag: String?,
deleted: Boolean, deleted: Boolean
): List<PersistedNoteMetadata> { ): List<PersistedNoteMetadata> {
require(limit > 0) { "limit should be positive" } require(limit > 0) { "limit should be positive" }
require(offset >= 0) { "offset should not be negative" } require(offset >= 0) { "offset should not be negative" }
@@ -50,7 +46,7 @@ internal class NoteRepositoryImpl(private val db: Database, private val converte
return notes.map { note -> return notes.map { note ->
val tags = tagsByUuid[note.uuid] ?: emptyList() val tags = tagsByUuid[note.uuid] ?: emptyList()
converter.toPersistedNoteMetadata(note, tags) note.toPersistedMetadata(tags)
} }
} }
@@ -60,7 +56,10 @@ internal class NoteRepositoryImpl(private val db: Database, private val converte
override fun create(userId: Int, note: Note): PersistedNote { override fun create(userId: Int, note: Note): PersistedNote {
val uuid = UUID.randomUUID() val uuid = UUID.randomUUID()
val entity = converter.toEntity(note, uuid, userId, LocalDateTime.now()) val entity = note.toEntity(uuid, userId).apply {
this.updatedAt = LocalDateTime.now()
}
db.useTransaction {
db.notes.add(entity) db.notes.add(entity)
db.batchInsert(Tags) { db.batchInsert(Tags) {
note.meta.tags.forEach { tagName -> note.meta.tags.forEach { tagName ->
@@ -70,7 +69,9 @@ internal class NoteRepositoryImpl(private val db: Database, private val converte
} }
} }
} }
return converter.toPersistedNote(entity, note.meta.tags) }
return entity.toPersistedNote(note.meta.tags)
} }
override fun find(userId: Int, uuid: UUID): PersistedNote? { override fun find(userId: Int, uuid: UUID): PersistedNote? {
@@ -85,10 +86,12 @@ internal class NoteRepositoryImpl(private val db: Database, private val converte
.where { Tags.noteUuid eq uuid } .where { Tags.noteUuid eq uuid }
.map { it[Tags.name]!! } .map { it[Tags.name]!! }
return converter.toPersistedNote(note, tags) return note.toPersistedNote(tags)
} }
override fun update(userId: Int, uuid: UUID, note: Note): PersistedNote? { override fun update(userId: Int, uuid: UUID, note: Note): PersistedNote? {
db.useTransaction {
val now = LocalDateTime.now() val now = LocalDateTime.now()
val count = db.update(Notes) { val count = db.update(Notes) {
it.title to note.meta.title it.title to note.meta.title
@@ -113,26 +116,39 @@ internal class NoteRepositoryImpl(private val db: Database, private val converte
} }
} }
return converter.toPersistedNote(note, now, uuid) return PersistedNote(
meta = note.meta,
markdown = note.markdown,
html = note.html,
updatedAt = now,
uuid = uuid,
public = false, // TODO
)
}
} }
override fun delete(userId: Int, uuid: UUID, permanent: Boolean): Boolean { override fun delete(userId: Int, uuid: UUID, permanent: Boolean): Boolean {
return if (!permanent) { return if (!permanent) {
db.useTransaction {
db.update(Notes) { db.update(Notes) {
it.deleted to true it.deleted to true
it.updatedAt to LocalDateTime.now() it.updatedAt to LocalDateTime.now()
where { it.userId eq userId and (it.uuid eq uuid) } where { it.userId eq userId and (it.uuid eq uuid) }
}
} == 1 } == 1
} else } else db.useTransaction {
db.delete(Notes) { it.uuid eq uuid and (it.userId eq userId) } == 1 db.delete(Notes) { it.uuid eq uuid and (it.userId eq userId) } == 1
} }
}
override fun restore(userId: Int, uuid: UUID): Boolean { override fun restore(userId: Int, uuid: UUID): Boolean {
return db.update(Notes) { return db.useTransaction {
db.update(Notes) {
it.deleted to false it.deleted to false
where { (it.userId eq userId) and (it.uuid eq uuid) } where { (it.userId eq userId) and (it.uuid eq uuid) }
} == 1 } == 1
} }
}
override fun getTags(userId: Int): List<String> = override fun getTags(userId: Int): List<String> =
db.from(Tags) db.from(Tags)
@@ -159,8 +175,14 @@ internal class NoteRepositoryImpl(private val db: Database, private val converte
val tagsByUuid = notes.tagsByUuid() val tagsByUuid = notes.tagsByUuid()
return notes.map { note -> return notes.map { note ->
val tags = tagsByUuid[note.uuid] ?: emptyList() ExportedNote(
converter.toExportedNote(note, tags) title = note.title,
tags = tagsByUuid[note.uuid] ?: emptyList(),
markdown = note.markdown,
html = note.html,
updatedAt = note.updatedAt,
trash = note.deleted,
)
} }
} }
@@ -174,7 +196,7 @@ internal class NoteRepositoryImpl(private val db: Database, private val converte
return notes.map { note -> return notes.map { note ->
val tags = tagsByUuid[note.uuid] ?: emptyList() val tags = tagsByUuid[note.uuid] ?: emptyList()
converter.toPersistedNote(note, tags) note.toPersistedNote(tags)
} }
} }
@@ -201,7 +223,7 @@ internal class NoteRepositoryImpl(private val db: Database, private val converte
.where { Tags.noteUuid eq uuid } .where { Tags.noteUuid eq uuid }
.map { it[Tags.name]!! } .map { it[Tags.name]!! }
return converter.toPersistedNote(note, tags) return note.toPersistedNote(tags)
} }
private fun List<NoteEntity>.tagsByUuid(): Map<UUID, List<String>> { private fun List<NoteEntity>.tagsByUuid(): Map<UUID, List<String>> {
@@ -2,12 +2,15 @@
package be.simplenotes.persistance.notes package be.simplenotes.persistance.notes
import be.simplenotes.domain.model.Note
import be.simplenotes.domain.model.NoteMetadata
import be.simplenotes.domain.model.PersistedNote
import be.simplenotes.domain.model.PersistedNoteMetadata
import be.simplenotes.persistance.extensions.uuidBinary import be.simplenotes.persistance.extensions.uuidBinary
import be.simplenotes.persistance.users.UserEntity import be.simplenotes.persistance.users.UserEntity
import be.simplenotes.persistance.users.Users import be.simplenotes.persistance.users.Users
import me.liuwj.ktorm.database.Database import me.liuwj.ktorm.database.*
import me.liuwj.ktorm.entity.Entity import me.liuwj.ktorm.entity.*
import me.liuwj.ktorm.entity.sequenceOf
import me.liuwj.ktorm.schema.* import me.liuwj.ktorm.schema.*
import java.time.LocalDateTime import java.time.LocalDateTime
import java.util.* import java.util.*
@@ -43,3 +46,21 @@ internal interface NoteEntity : Entity<NoteEntity> {
var user: UserEntity var user: UserEntity
} }
internal fun NoteEntity.toPersistedMetadata(tags: List<String>) = PersistedNoteMetadata(title, tags, updatedAt, uuid)
internal fun NoteEntity.toPersistedNote(tags: List<String>) =
PersistedNote(NoteMetadata(title, tags), markdown, html, updatedAt, uuid, public)
internal fun Note.toEntity(uuid: UUID, userId: Int): NoteEntity {
val note = this
return NoteEntity {
this.title = note.meta.title
this.markdown = note.markdown
this.html = note.html
this.uuid = uuid
this.deleted = false
this.public = false
this.user["id"] = userId
}
}
@@ -0,0 +1,32 @@
package be.simplenotes.persistance.users
import be.simplenotes.domain.model.PersistedUser
import be.simplenotes.domain.model.User
import be.simplenotes.domain.usecases.repositories.UserRepository
import me.liuwj.ktorm.database.*
import me.liuwj.ktorm.dsl.*
import me.liuwj.ktorm.entity.*
import java.sql.SQLIntegrityConstraintViolationException
internal class UserRepositoryImpl(private val db: Database) : UserRepository {
override fun create(user: User): PersistedUser? {
return try {
db.useTransaction {
val id = db.insertAndGenerateKey(Users) {
it.username to user.username
it.password to user.password
} as Int
PersistedUser(user.username, user.password, id)
}
} catch (e: SQLIntegrityConstraintViolationException) {
null
}
}
override fun find(username: String) = db.users.find { it.username eq username }?.toPersistedUser()
override fun find(id: Int) = db.users.find { it.id eq id }?.toPersistedUser()
override fun exists(username: String) = db.users.any { it.username eq username }
override fun exists(id: Int) = db.users.any { it.id eq id }
override fun delete(id: Int) = db.useTransaction { db.delete(Users) { it.id eq id } == 1 }
override fun findAll() = db.from(Users).select(Users.id).map { it[Users.id]!! }
}
@@ -1,11 +1,9 @@
package be.simplenotes.persistance.users package be.simplenotes.persistance.users
import me.liuwj.ktorm.database.Database import be.simplenotes.domain.model.PersistedUser
import me.liuwj.ktorm.entity.Entity import me.liuwj.ktorm.database.*
import me.liuwj.ktorm.entity.sequenceOf import me.liuwj.ktorm.entity.*
import me.liuwj.ktorm.schema.Table import me.liuwj.ktorm.schema.*
import me.liuwj.ktorm.schema.int
import me.liuwj.ktorm.schema.varchar
internal open class Users(alias: String?) : Table<UserEntity>("Users", alias) { internal open class Users(alias: String?) : Table<UserEntity>("Users", alias) {
companion object : Users(null) companion object : Users(null)
@@ -20,9 +18,11 @@ internal open class Users(alias: String?) : Table<UserEntity>("Users", alias) {
internal interface UserEntity : Entity<UserEntity> { internal interface UserEntity : Entity<UserEntity> {
companion object : Entity.Factory<UserEntity>() companion object : Entity.Factory<UserEntity>()
var id: Int val id: Int
var username: String var username: String
var password: String var password: String
} }
internal fun UserEntity.toPersistedUser() = PersistedUser(username, password, id)
internal val Database.users get() = this.sequenceOf(Users, withReferences = false) internal val Database.users get() = this.sequenceOf(Users, withReferences = false)

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